expo-iap 3.1.7 → 3.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +92 -0
- package/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt +69 -2
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +62 -4
- package/build/index.d.ts +32 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +40 -15
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +68 -0
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +74 -0
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +23 -0
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +34 -3
- package/build/modules/ios.js.map +1 -1
- package/build/types.d.ts +116 -13
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/useIAP.d.ts +8 -0
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +11 -1
- package/build/useIAP.js.map +1 -1
- package/bun.lockb +0 -0
- package/coverage/clover.xml +258 -234
- package/coverage/coverage-final.json +3 -3
- package/coverage/lcov-report/index.html +27 -27
- package/coverage/lcov-report/src/helpers/index.html +1 -1
- package/coverage/lcov-report/src/helpers/subscription.ts.html +1 -1
- package/coverage/lcov-report/src/index.html +19 -19
- package/coverage/lcov-report/src/index.ts.html +136 -31
- package/coverage/lcov-report/src/modules/android.ts.html +257 -8
- package/coverage/lcov-report/src/modules/index.html +23 -23
- package/coverage/lcov-report/src/modules/ios.ts.html +137 -11
- package/coverage/lcov-report/src/utils/debug.ts.html +1 -1
- package/coverage/lcov-report/src/utils/errorMapping.ts.html +1 -1
- package/coverage/lcov-report/src/utils/index.html +1 -1
- package/coverage/lcov.info +473 -429
- package/ios/ExpoIapHelper.swift +4 -0
- package/ios/ExpoIapModule.swift +34 -2
- package/openiap-versions.json +3 -3
- package/package.json +1 -1
- package/plugin/build/withIAP.d.ts +26 -0
- package/plugin/build/withIAP.js +67 -3
- package/plugin/build/withLocalOpenIAP.d.ts +2 -0
- package/plugin/build/withLocalOpenIAP.js +7 -0
- package/plugin/src/withIAP.ts +141 -3
- package/plugin/src/withLocalOpenIAP.ts +14 -4
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/index.ts +49 -14
- package/src/modules/android.ts +83 -0
- package/src/modules/ios.ts +45 -3
- package/src/types.ts +124 -13
- package/src/useIAP.ts +26 -1
package/ios/ExpoIapHelper.swift
CHANGED
|
@@ -86,6 +86,10 @@ enum ExpoIapHelper {
|
|
|
86
86
|
case .all:
|
|
87
87
|
break
|
|
88
88
|
}
|
|
89
|
+
// Include useAlternativeBilling if present
|
|
90
|
+
if let useAlternativeBilling = payload["useAlternativeBilling"] {
|
|
91
|
+
normalized["useAlternativeBilling"] = useAlternativeBilling
|
|
92
|
+
}
|
|
89
93
|
return try OpenIapSerialization.decode(
|
|
90
94
|
object: normalized, as: RequestPurchaseProps.self)
|
|
91
95
|
}
|
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -13,7 +13,7 @@ public final class ExpoIapModule: Module {
|
|
|
13
13
|
nonisolated public func definition() -> ModuleDefinition {
|
|
14
14
|
Name("ExpoIap")
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Property("errorCodes") {
|
|
17
17
|
OpenIapSerialization.errorCodes()
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -35,7 +35,9 @@ public final class ExpoIapModule: Module {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
AsyncFunction("initConnection") { () async throws -> Bool in
|
|
38
|
+
AsyncFunction("initConnection") { (config: [String: Any]?) async throws -> Bool in
|
|
39
|
+
// Note: iOS doesn't support alternative billing config parameter
|
|
40
|
+
// Config is ignored on iOS platform
|
|
39
41
|
let isConnected = try await OpenIapModule.shared.initConnection()
|
|
40
42
|
await MainActor.run { self.isInitialized = isConnected }
|
|
41
43
|
return isConnected
|
|
@@ -59,8 +61,10 @@ public final class ExpoIapModule: Module {
|
|
|
59
61
|
|
|
60
62
|
AsyncFunction("requestPurchase") { (payload: [String: Any]) async throws -> Any? in
|
|
61
63
|
ExpoIapLog.payload("requestPurchase", payload: payload)
|
|
64
|
+
print("🔍 [ExpoIap] Raw payload useAlternativeBilling: \(payload["useAlternativeBilling"] ?? "nil")")
|
|
62
65
|
try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
|
|
63
66
|
let props = try ExpoIapHelper.decodeRequestPurchaseProps(from: payload)
|
|
67
|
+
print("🔍 [ExpoIap] Decoded props useAlternativeBilling: \(props.useAlternativeBilling ?? false)")
|
|
64
68
|
|
|
65
69
|
do {
|
|
66
70
|
guard let result = try await OpenIapModule.shared.requestPurchase(props) else {
|
|
@@ -334,5 +338,33 @@ public final class ExpoIapModule: Module {
|
|
|
334
338
|
throw PurchaseError.make(code: .skuNotFound, productId: sku)
|
|
335
339
|
}
|
|
336
340
|
}
|
|
341
|
+
|
|
342
|
+
// MARK: - External Purchase (iOS 16.0+)
|
|
343
|
+
|
|
344
|
+
AsyncFunction("canPresentExternalPurchaseNoticeIOS") { () async throws -> Bool in
|
|
345
|
+
ExpoIapLog.payload("canPresentExternalPurchaseNoticeIOS", payload: nil)
|
|
346
|
+
try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
|
|
347
|
+
let canPresent = try await OpenIapModule.shared.canPresentExternalPurchaseNoticeIOS()
|
|
348
|
+
ExpoIapLog.result("canPresentExternalPurchaseNoticeIOS", value: canPresent)
|
|
349
|
+
return canPresent
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
AsyncFunction("presentExternalPurchaseNoticeSheetIOS") { () async throws -> [String: Any] in
|
|
353
|
+
ExpoIapLog.payload("presentExternalPurchaseNoticeSheetIOS", payload: nil)
|
|
354
|
+
try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
|
|
355
|
+
let result = try await OpenIapModule.shared.presentExternalPurchaseNoticeSheetIOS()
|
|
356
|
+
let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
|
|
357
|
+
ExpoIapLog.result("presentExternalPurchaseNoticeSheetIOS", value: sanitized)
|
|
358
|
+
return sanitized
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
AsyncFunction("presentExternalPurchaseLinkIOS") { (url: String) async throws -> [String: Any] in
|
|
362
|
+
ExpoIapLog.payload("presentExternalPurchaseLinkIOS", payload: ["url": url])
|
|
363
|
+
try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
|
|
364
|
+
let result = try await OpenIapModule.shared.presentExternalPurchaseLinkIOS(url)
|
|
365
|
+
let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
|
|
366
|
+
ExpoIapLog.result("presentExternalPurchaseLinkIOS", value: sanitized)
|
|
367
|
+
return sanitized
|
|
368
|
+
}
|
|
337
369
|
}
|
|
338
370
|
}
|
package/openiap-versions.json
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
+
export interface IOSAlternativeBillingConfig {
|
|
3
|
+
/** Country codes where external purchases are supported (ISO 3166-1 alpha-2) */
|
|
4
|
+
countries?: string[];
|
|
5
|
+
/** External purchase URLs per country (iOS 15.4+) */
|
|
6
|
+
links?: Record<string, string>;
|
|
7
|
+
/** Multiple external purchase URLs per country (iOS 17.5+, up to 5 per country) */
|
|
8
|
+
multiLinks?: Record<string, string[]>;
|
|
9
|
+
/** Custom link regions (iOS 18.1+) */
|
|
10
|
+
customLinkRegions?: string[];
|
|
11
|
+
/** Streaming link regions for music apps (iOS 18.2+) */
|
|
12
|
+
streamingLinkRegions?: string[];
|
|
13
|
+
/** Enable external purchase link entitlement */
|
|
14
|
+
enableExternalPurchaseLink?: boolean;
|
|
15
|
+
/** Enable external purchase link streaming entitlement (music apps only) */
|
|
16
|
+
enableExternalPurchaseLinkStreaming?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Add external purchase entitlements and Info.plist configuration */
|
|
19
|
+
declare const withIosAlternativeBilling: ConfigPlugin<IOSAlternativeBillingConfig | undefined>;
|
|
2
20
|
export interface ExpoIapPluginOptions {
|
|
3
21
|
/** Local development path for OpenIAP library */
|
|
4
22
|
localPath?: string | {
|
|
@@ -7,6 +25,14 @@ export interface ExpoIapPluginOptions {
|
|
|
7
25
|
};
|
|
8
26
|
/** Enable local development mode */
|
|
9
27
|
enableLocalDev?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* iOS Alternative Billing configuration.
|
|
30
|
+
* Configure external purchase countries, links, and entitlements.
|
|
31
|
+
* Requires approval from Apple.
|
|
32
|
+
* @platform ios
|
|
33
|
+
*/
|
|
34
|
+
iosAlternativeBilling?: IOSAlternativeBillingConfig;
|
|
10
35
|
}
|
|
36
|
+
export { withIosAlternativeBilling };
|
|
11
37
|
declare const _default: ConfigPlugin<void | ExpoIapPluginOptions>;
|
|
12
38
|
export default _default;
|
package/plugin/build/withIAP.js
CHANGED
|
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.withIosAlternativeBilling = void 0;
|
|
39
40
|
const config_plugins_1 = require("expo/config-plugins");
|
|
40
41
|
const fs = __importStar(require("fs"));
|
|
41
42
|
const path = __importStar(require("path"));
|
|
@@ -117,8 +118,68 @@ const withIapAndroid = (config, props) => {
|
|
|
117
118
|
});
|
|
118
119
|
return config;
|
|
119
120
|
};
|
|
121
|
+
/** Add external purchase entitlements and Info.plist configuration */
|
|
122
|
+
const withIosAlternativeBilling = (config, options) => {
|
|
123
|
+
if (!options || !options.countries || options.countries.length === 0) {
|
|
124
|
+
return config;
|
|
125
|
+
}
|
|
126
|
+
// Add entitlements
|
|
127
|
+
config = (0, config_plugins_1.withEntitlementsPlist)(config, (config) => {
|
|
128
|
+
// Always add basic external purchase entitlement when countries are specified
|
|
129
|
+
config.modResults['com.apple.developer.storekit.external-purchase'] = true;
|
|
130
|
+
logOnce('✅ Added com.apple.developer.storekit.external-purchase to entitlements');
|
|
131
|
+
// Add external purchase link entitlement if enabled
|
|
132
|
+
if (options.enableExternalPurchaseLink) {
|
|
133
|
+
config.modResults['com.apple.developer.storekit.external-purchase-link'] =
|
|
134
|
+
true;
|
|
135
|
+
logOnce('✅ Added com.apple.developer.storekit.external-purchase-link to entitlements');
|
|
136
|
+
}
|
|
137
|
+
// Add streaming entitlement if enabled
|
|
138
|
+
if (options.enableExternalPurchaseLinkStreaming) {
|
|
139
|
+
config.modResults['com.apple.developer.storekit.external-purchase-link-streaming'] = true;
|
|
140
|
+
logOnce('✅ Added com.apple.developer.storekit.external-purchase-link-streaming to entitlements');
|
|
141
|
+
}
|
|
142
|
+
return config;
|
|
143
|
+
});
|
|
144
|
+
// Add Info.plist configuration
|
|
145
|
+
config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
|
|
146
|
+
const plist = config.modResults;
|
|
147
|
+
// 1. SKExternalPurchase (Required)
|
|
148
|
+
plist.SKExternalPurchase = options.countries;
|
|
149
|
+
logOnce(`✅ Added SKExternalPurchase with countries: ${options.countries?.join(', ')}`);
|
|
150
|
+
// 2. SKExternalPurchaseLink (Optional - iOS 15.4+)
|
|
151
|
+
if (options.links && Object.keys(options.links).length > 0) {
|
|
152
|
+
plist.SKExternalPurchaseLink = options.links;
|
|
153
|
+
logOnce(`✅ Added SKExternalPurchaseLink for ${Object.keys(options.links).length} countries`);
|
|
154
|
+
}
|
|
155
|
+
// 3. SKExternalPurchaseMultiLink (iOS 17.5+)
|
|
156
|
+
if (options.multiLinks && Object.keys(options.multiLinks).length > 0) {
|
|
157
|
+
plist.SKExternalPurchaseMultiLink = options.multiLinks;
|
|
158
|
+
logOnce(`✅ Added SKExternalPurchaseMultiLink for ${Object.keys(options.multiLinks).length} countries`);
|
|
159
|
+
}
|
|
160
|
+
// 4. SKExternalPurchaseCustomLinkRegions (iOS 18.1+)
|
|
161
|
+
if (options.customLinkRegions && options.customLinkRegions.length > 0) {
|
|
162
|
+
plist.SKExternalPurchaseCustomLinkRegions = options.customLinkRegions;
|
|
163
|
+
logOnce(`✅ Added SKExternalPurchaseCustomLinkRegions: ${options.customLinkRegions.join(', ')}`);
|
|
164
|
+
}
|
|
165
|
+
// 5. SKExternalPurchaseLinkStreamingRegions (iOS 18.2+)
|
|
166
|
+
if (options.streamingLinkRegions &&
|
|
167
|
+
options.streamingLinkRegions.length > 0) {
|
|
168
|
+
plist.SKExternalPurchaseLinkStreamingRegions =
|
|
169
|
+
options.streamingLinkRegions;
|
|
170
|
+
logOnce(`✅ Added SKExternalPurchaseLinkStreamingRegions: ${options.streamingLinkRegions.join(', ')}`);
|
|
171
|
+
}
|
|
172
|
+
return config;
|
|
173
|
+
});
|
|
174
|
+
return config;
|
|
175
|
+
};
|
|
176
|
+
exports.withIosAlternativeBilling = withIosAlternativeBilling;
|
|
120
177
|
/** Ensure Podfile uses CocoaPods CDN and no stale local OpenIAP entry remains. */
|
|
121
|
-
const withIapIOS = (config) => {
|
|
178
|
+
const withIapIOS = (config, options) => {
|
|
179
|
+
// Add iOS alternative billing configuration if provided
|
|
180
|
+
if (options) {
|
|
181
|
+
config = withIosAlternativeBilling(config, options);
|
|
182
|
+
}
|
|
122
183
|
return (0, config_plugins_1.withDangerousMod)(config, [
|
|
123
184
|
'ios',
|
|
124
185
|
async (config) => {
|
|
@@ -168,12 +229,15 @@ const withIap = (config, options) => {
|
|
|
168
229
|
? resolved
|
|
169
230
|
: `ios=${resolved.ios ?? 'auto'}, android=${resolved.android ?? 'auto'}`;
|
|
170
231
|
logOnce(`🔧 [expo-iap] Enabling local OpenIAP: ${preview}`);
|
|
171
|
-
result = (0, withLocalOpenIAP_1.default)(result, {
|
|
232
|
+
result = (0, withLocalOpenIAP_1.default)(result, {
|
|
233
|
+
localPath: resolved,
|
|
234
|
+
iosAlternativeBilling: options?.iosAlternativeBilling,
|
|
235
|
+
});
|
|
172
236
|
}
|
|
173
237
|
}
|
|
174
238
|
else {
|
|
175
239
|
// Ensure iOS Podfile is set up to resolve public CocoaPods specs
|
|
176
|
-
result = withIapIOS(result);
|
|
240
|
+
result = withIapIOS(result, options?.iosAlternativeBilling);
|
|
177
241
|
logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
|
|
178
242
|
}
|
|
179
243
|
return result;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
+
import type { IOSAlternativeBillingConfig } from './withIAP';
|
|
2
3
|
/**
|
|
3
4
|
* Plugin to add local OpenIAP pod dependency for development
|
|
4
5
|
* This is only for local development with openiap-apple library
|
|
@@ -9,5 +10,6 @@ type LocalPathOption = string | {
|
|
|
9
10
|
};
|
|
10
11
|
declare const withLocalOpenIAP: ConfigPlugin<{
|
|
11
12
|
localPath?: LocalPathOption;
|
|
13
|
+
iosAlternativeBilling?: IOSAlternativeBillingConfig;
|
|
12
14
|
} | void>;
|
|
13
15
|
export default withLocalOpenIAP;
|
|
@@ -37,6 +37,13 @@ const config_plugins_1 = require("expo/config-plugins");
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const withLocalOpenIAP = (config, props) => {
|
|
40
|
+
// Import and apply iOS alternative billing configuration if provided
|
|
41
|
+
if (props?.iosAlternativeBilling) {
|
|
42
|
+
// Import withIosAlternativeBilling from withIAP module
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
|
+
const { withIosAlternativeBilling } = require('./withIAP');
|
|
45
|
+
config = withIosAlternativeBilling(config, props.iosAlternativeBilling);
|
|
46
|
+
}
|
|
40
47
|
// Helper to resolve Android module path
|
|
41
48
|
const resolveAndroidModulePath = (p) => {
|
|
42
49
|
if (!p)
|
package/plugin/src/withIAP.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
withAndroidManifest,
|
|
6
6
|
withAppBuildGradle,
|
|
7
7
|
withDangerousMod,
|
|
8
|
+
withEntitlementsPlist,
|
|
9
|
+
withInfoPlist,
|
|
8
10
|
} from 'expo/config-plugins';
|
|
9
11
|
import * as fs from 'fs';
|
|
10
12
|
import * as path from 'path';
|
|
@@ -137,8 +139,133 @@ const withIapAndroid: ConfigPlugin<{addDeps?: boolean} | void> = (
|
|
|
137
139
|
return config;
|
|
138
140
|
};
|
|
139
141
|
|
|
142
|
+
export interface IOSAlternativeBillingConfig {
|
|
143
|
+
/** Country codes where external purchases are supported (ISO 3166-1 alpha-2) */
|
|
144
|
+
countries?: string[];
|
|
145
|
+
/** External purchase URLs per country (iOS 15.4+) */
|
|
146
|
+
links?: Record<string, string>;
|
|
147
|
+
/** Multiple external purchase URLs per country (iOS 17.5+, up to 5 per country) */
|
|
148
|
+
multiLinks?: Record<string, string[]>;
|
|
149
|
+
/** Custom link regions (iOS 18.1+) */
|
|
150
|
+
customLinkRegions?: string[];
|
|
151
|
+
/** Streaming link regions for music apps (iOS 18.2+) */
|
|
152
|
+
streamingLinkRegions?: string[];
|
|
153
|
+
/** Enable external purchase link entitlement */
|
|
154
|
+
enableExternalPurchaseLink?: boolean;
|
|
155
|
+
/** Enable external purchase link streaming entitlement (music apps only) */
|
|
156
|
+
enableExternalPurchaseLinkStreaming?: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Add external purchase entitlements and Info.plist configuration */
|
|
160
|
+
const withIosAlternativeBilling: ConfigPlugin<
|
|
161
|
+
IOSAlternativeBillingConfig | undefined
|
|
162
|
+
> = (config, options) => {
|
|
163
|
+
if (!options || !options.countries || options.countries.length === 0) {
|
|
164
|
+
return config;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Add entitlements
|
|
168
|
+
config = withEntitlementsPlist(config, (config) => {
|
|
169
|
+
// Always add basic external purchase entitlement when countries are specified
|
|
170
|
+
config.modResults['com.apple.developer.storekit.external-purchase'] = true;
|
|
171
|
+
logOnce(
|
|
172
|
+
'✅ Added com.apple.developer.storekit.external-purchase to entitlements',
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Add external purchase link entitlement if enabled
|
|
176
|
+
if (options.enableExternalPurchaseLink) {
|
|
177
|
+
config.modResults['com.apple.developer.storekit.external-purchase-link'] =
|
|
178
|
+
true;
|
|
179
|
+
logOnce(
|
|
180
|
+
'✅ Added com.apple.developer.storekit.external-purchase-link to entitlements',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Add streaming entitlement if enabled
|
|
185
|
+
if (options.enableExternalPurchaseLinkStreaming) {
|
|
186
|
+
config.modResults[
|
|
187
|
+
'com.apple.developer.storekit.external-purchase-link-streaming'
|
|
188
|
+
] = true;
|
|
189
|
+
logOnce(
|
|
190
|
+
'✅ Added com.apple.developer.storekit.external-purchase-link-streaming to entitlements',
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return config;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Add Info.plist configuration
|
|
198
|
+
config = withInfoPlist(config, (config) => {
|
|
199
|
+
const plist = config.modResults;
|
|
200
|
+
|
|
201
|
+
// 1. SKExternalPurchase (Required)
|
|
202
|
+
plist.SKExternalPurchase = options.countries;
|
|
203
|
+
logOnce(
|
|
204
|
+
`✅ Added SKExternalPurchase with countries: ${options.countries?.join(
|
|
205
|
+
', ',
|
|
206
|
+
)}`,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// 2. SKExternalPurchaseLink (Optional - iOS 15.4+)
|
|
210
|
+
if (options.links && Object.keys(options.links).length > 0) {
|
|
211
|
+
plist.SKExternalPurchaseLink = options.links;
|
|
212
|
+
logOnce(
|
|
213
|
+
`✅ Added SKExternalPurchaseLink for ${
|
|
214
|
+
Object.keys(options.links).length
|
|
215
|
+
} countries`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 3. SKExternalPurchaseMultiLink (iOS 17.5+)
|
|
220
|
+
if (options.multiLinks && Object.keys(options.multiLinks).length > 0) {
|
|
221
|
+
plist.SKExternalPurchaseMultiLink = options.multiLinks;
|
|
222
|
+
logOnce(
|
|
223
|
+
`✅ Added SKExternalPurchaseMultiLink for ${
|
|
224
|
+
Object.keys(options.multiLinks).length
|
|
225
|
+
} countries`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 4. SKExternalPurchaseCustomLinkRegions (iOS 18.1+)
|
|
230
|
+
if (options.customLinkRegions && options.customLinkRegions.length > 0) {
|
|
231
|
+
plist.SKExternalPurchaseCustomLinkRegions = options.customLinkRegions;
|
|
232
|
+
logOnce(
|
|
233
|
+
`✅ Added SKExternalPurchaseCustomLinkRegions: ${options.customLinkRegions.join(
|
|
234
|
+
', ',
|
|
235
|
+
)}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 5. SKExternalPurchaseLinkStreamingRegions (iOS 18.2+)
|
|
240
|
+
if (
|
|
241
|
+
options.streamingLinkRegions &&
|
|
242
|
+
options.streamingLinkRegions.length > 0
|
|
243
|
+
) {
|
|
244
|
+
plist.SKExternalPurchaseLinkStreamingRegions =
|
|
245
|
+
options.streamingLinkRegions;
|
|
246
|
+
logOnce(
|
|
247
|
+
`✅ Added SKExternalPurchaseLinkStreamingRegions: ${options.streamingLinkRegions.join(
|
|
248
|
+
', ',
|
|
249
|
+
)}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return config;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return config;
|
|
257
|
+
};
|
|
258
|
+
|
|
140
259
|
/** Ensure Podfile uses CocoaPods CDN and no stale local OpenIAP entry remains. */
|
|
141
|
-
const withIapIOS: ConfigPlugin = (
|
|
260
|
+
const withIapIOS: ConfigPlugin<IOSAlternativeBillingConfig | undefined> = (
|
|
261
|
+
config,
|
|
262
|
+
options,
|
|
263
|
+
) => {
|
|
264
|
+
// Add iOS alternative billing configuration if provided
|
|
265
|
+
if (options) {
|
|
266
|
+
config = withIosAlternativeBilling(config, options);
|
|
267
|
+
}
|
|
268
|
+
|
|
142
269
|
return withDangerousMod(config, [
|
|
143
270
|
'ios',
|
|
144
271
|
async (config) => {
|
|
@@ -182,6 +309,13 @@ export interface ExpoIapPluginOptions {
|
|
|
182
309
|
};
|
|
183
310
|
/** Enable local development mode */
|
|
184
311
|
enableLocalDev?: boolean;
|
|
312
|
+
/**
|
|
313
|
+
* iOS Alternative Billing configuration.
|
|
314
|
+
* Configure external purchase countries, links, and entitlements.
|
|
315
|
+
* Requires approval from Apple.
|
|
316
|
+
* @platform ios
|
|
317
|
+
*/
|
|
318
|
+
iosAlternativeBilling?: IOSAlternativeBillingConfig;
|
|
185
319
|
}
|
|
186
320
|
|
|
187
321
|
const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
|
|
@@ -218,11 +352,14 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
|
|
|
218
352
|
resolved.android ?? 'auto'
|
|
219
353
|
}`;
|
|
220
354
|
logOnce(`🔧 [expo-iap] Enabling local OpenIAP: ${preview}`);
|
|
221
|
-
result = withLocalOpenIAP(result, {
|
|
355
|
+
result = withLocalOpenIAP(result, {
|
|
356
|
+
localPath: resolved,
|
|
357
|
+
iosAlternativeBilling: options?.iosAlternativeBilling,
|
|
358
|
+
});
|
|
222
359
|
}
|
|
223
360
|
} else {
|
|
224
361
|
// Ensure iOS Podfile is set up to resolve public CocoaPods specs
|
|
225
|
-
result = withIapIOS(result);
|
|
362
|
+
result = withIapIOS(result, options?.iosAlternativeBilling);
|
|
226
363
|
logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
|
|
227
364
|
}
|
|
228
365
|
|
|
@@ -237,4 +374,5 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
|
|
|
237
374
|
}
|
|
238
375
|
};
|
|
239
376
|
|
|
377
|
+
export {withIosAlternativeBilling};
|
|
240
378
|
export default createRunOncePlugin(withIap, pkg.name, pkg.version);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from 'expo/config-plugins';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
+
import type {IOSAlternativeBillingConfig} from './withIAP';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Plugin to add local OpenIAP pod dependency for development
|
|
@@ -13,10 +14,19 @@ import * as path from 'path';
|
|
|
13
14
|
*/
|
|
14
15
|
type LocalPathOption = string | {ios?: string; android?: string};
|
|
15
16
|
|
|
16
|
-
const withLocalOpenIAP: ConfigPlugin<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const withLocalOpenIAP: ConfigPlugin<
|
|
18
|
+
{
|
|
19
|
+
localPath?: LocalPathOption;
|
|
20
|
+
iosAlternativeBilling?: IOSAlternativeBillingConfig;
|
|
21
|
+
} | void
|
|
22
|
+
> = (config, props) => {
|
|
23
|
+
// Import and apply iOS alternative billing configuration if provided
|
|
24
|
+
if (props?.iosAlternativeBilling) {
|
|
25
|
+
// Import withIosAlternativeBilling from withIAP module
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
const {withIosAlternativeBilling} = require('./withIAP');
|
|
28
|
+
config = withIosAlternativeBilling(config, props.iosAlternativeBilling);
|
|
29
|
+
}
|
|
20
30
|
// Helper to resolve Android module path
|
|
21
31
|
const resolveAndroidModulePath = (p?: string): string | null => {
|
|
22
32
|
if (!p) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/expoConfig.augmentation.d.ts","./src/withIAP.ts","./src/withLocalOpenIAP.ts"],"version":"5.9.
|
|
1
|
+
{"root":["./src/expoConfig.augmentation.d.ts","./src/withIAP.ts","./src/withLocalOpenIAP.ts"],"version":"5.9.3"}
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import type {
|
|
|
37
37
|
RequestSubscriptionPropsByPlatforms,
|
|
38
38
|
RequestSubscriptionAndroidProps,
|
|
39
39
|
RequestSubscriptionIosProps,
|
|
40
|
+
UserChoiceBillingDetails,
|
|
40
41
|
} from './types';
|
|
41
42
|
import {ErrorCode} from './types';
|
|
42
43
|
import {createPurchaseError, type PurchaseError} from './utils/errorMapping';
|
|
@@ -57,12 +58,14 @@ export enum OpenIapEvent {
|
|
|
57
58
|
PurchaseUpdated = 'purchase-updated',
|
|
58
59
|
PurchaseError = 'purchase-error',
|
|
59
60
|
PromotedProductIOS = 'promoted-product-ios',
|
|
61
|
+
UserChoiceBillingAndroid = 'user-choice-billing-android',
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
type ExpoIapEventPayloads = {
|
|
63
65
|
[OpenIapEvent.PurchaseUpdated]: Purchase;
|
|
64
66
|
[OpenIapEvent.PurchaseError]: PurchaseError;
|
|
65
67
|
[OpenIapEvent.PromotedProductIOS]: Product;
|
|
68
|
+
[OpenIapEvent.UserChoiceBillingAndroid]: UserChoiceBillingDetails;
|
|
66
69
|
};
|
|
67
70
|
|
|
68
71
|
type ExpoIapEventListener<E extends OpenIapEvent> = (
|
|
@@ -193,8 +196,45 @@ export const promotedProductListenerIOS = (
|
|
|
193
196
|
return emitter.addListener(OpenIapEvent.PromotedProductIOS, listener);
|
|
194
197
|
};
|
|
195
198
|
|
|
196
|
-
|
|
197
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Android-only listener for User Choice Billing events.
|
|
201
|
+
* This fires when a user selects alternative billing instead of Google Play billing
|
|
202
|
+
* in the User Choice Billing dialog (only in 'user-choice' mode).
|
|
203
|
+
*
|
|
204
|
+
* @param listener - Callback function that receives the external transaction token and product IDs
|
|
205
|
+
* @returns EventSubscription that can be used to unsubscribe
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const subscription = userChoiceBillingListenerAndroid((details) => {
|
|
210
|
+
* console.log('User selected alternative billing');
|
|
211
|
+
* console.log('Token:', details.externalTransactionToken);
|
|
212
|
+
* console.log('Products:', details.products);
|
|
213
|
+
*
|
|
214
|
+
* // Process payment in your system, then report token to Google
|
|
215
|
+
* await processPaymentAndReportToken(details);
|
|
216
|
+
* });
|
|
217
|
+
*
|
|
218
|
+
* // Later, clean up
|
|
219
|
+
* subscription.remove();
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* @platform Android
|
|
223
|
+
*/
|
|
224
|
+
export const userChoiceBillingListenerAndroid = (
|
|
225
|
+
listener: (details: UserChoiceBillingDetails) => void,
|
|
226
|
+
) => {
|
|
227
|
+
if (Platform.OS !== 'android') {
|
|
228
|
+
ExpoIapConsole.warn(
|
|
229
|
+
'userChoiceBillingListenerAndroid: This listener is only available on Android',
|
|
230
|
+
);
|
|
231
|
+
return {remove: () => {}};
|
|
232
|
+
}
|
|
233
|
+
return emitter.addListener(OpenIapEvent.UserChoiceBillingAndroid, listener);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const initConnection: MutationField<'initConnection'> = async (config) =>
|
|
237
|
+
ExpoIapModule.initConnection(config ?? null);
|
|
198
238
|
|
|
199
239
|
export const endConnection: MutationField<'endConnection'> = async () =>
|
|
200
240
|
ExpoIapModule.endConnection();
|
|
@@ -375,21 +415,16 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
375
415
|
);
|
|
376
416
|
}
|
|
377
417
|
|
|
378
|
-
|
|
379
|
-
if (canonical === 'in-app') {
|
|
380
|
-
payload = {
|
|
381
|
-
type: 'in-app',
|
|
382
|
-
request: request as RequestPurchasePropsByPlatforms,
|
|
383
|
-
};
|
|
384
|
-
} else if (canonical === 'subs') {
|
|
385
|
-
payload = {
|
|
386
|
-
type: 'subs',
|
|
387
|
-
request: request as RequestSubscriptionPropsByPlatforms,
|
|
388
|
-
};
|
|
389
|
-
} else {
|
|
418
|
+
if (canonical !== 'in-app' && canonical !== 'subs') {
|
|
390
419
|
throw new Error(`Unsupported product type: ${canonical}`);
|
|
391
420
|
}
|
|
392
421
|
|
|
422
|
+
const payload: MutationRequestPurchaseArgs = {
|
|
423
|
+
type: canonical === 'in-app' ? 'in-app' : 'subs',
|
|
424
|
+
request,
|
|
425
|
+
useAlternativeBilling: args.useAlternativeBilling,
|
|
426
|
+
};
|
|
427
|
+
|
|
393
428
|
const purchase = (await ExpoIapModule.requestPurchase(payload)) as
|
|
394
429
|
| Purchase
|
|
395
430
|
| Purchase[]
|
package/src/modules/android.ts
CHANGED
|
@@ -163,3 +163,86 @@ export const acknowledgePurchaseAndroid: MutationField<
|
|
|
163
163
|
export const openRedeemOfferCodeAndroid = async (): Promise<void> => {
|
|
164
164
|
return Linking.openURL(`https://play.google.com/redeem?code=`);
|
|
165
165
|
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if alternative billing is available for this user/device (Android only).
|
|
169
|
+
* Step 1 of alternative billing flow.
|
|
170
|
+
*
|
|
171
|
+
* Returns true if available, false otherwise.
|
|
172
|
+
* Throws OpenIapError.NotPrepared if billing client not ready.
|
|
173
|
+
*
|
|
174
|
+
* @returns {Promise<boolean>}
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* const isAvailable = await checkAlternativeBillingAvailabilityAndroid();
|
|
179
|
+
* if (isAvailable) {
|
|
180
|
+
* // Proceed with alternative billing flow
|
|
181
|
+
* }
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export const checkAlternativeBillingAvailabilityAndroid: MutationField<
|
|
185
|
+
'checkAlternativeBillingAvailabilityAndroid'
|
|
186
|
+
> = async () => {
|
|
187
|
+
return ExpoIapModule.checkAlternativeBillingAvailabilityAndroid();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Show alternative billing information dialog to user (Android only).
|
|
192
|
+
* Step 2 of alternative billing flow.
|
|
193
|
+
* Must be called BEFORE processing payment in your payment system.
|
|
194
|
+
*
|
|
195
|
+
* Returns true if user accepted, false if user canceled.
|
|
196
|
+
* Throws OpenIapError.NotPrepared if billing client not ready.
|
|
197
|
+
*
|
|
198
|
+
* @returns {Promise<boolean>}
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* const userAccepted = await showAlternativeBillingDialogAndroid();
|
|
203
|
+
* if (userAccepted) {
|
|
204
|
+
* // Process payment in your payment system
|
|
205
|
+
* const success = await processCustomPayment();
|
|
206
|
+
* if (success) {
|
|
207
|
+
* // Create reporting token
|
|
208
|
+
* const token = await createAlternativeBillingTokenAndroid();
|
|
209
|
+
* // Send token to your backend for Google Play reporting
|
|
210
|
+
* }
|
|
211
|
+
* }
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export const showAlternativeBillingDialogAndroid: MutationField<
|
|
215
|
+
'showAlternativeBillingDialogAndroid'
|
|
216
|
+
> = async () => {
|
|
217
|
+
return ExpoIapModule.showAlternativeBillingDialogAndroid();
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Create external transaction token for Google Play reporting (Android only).
|
|
222
|
+
* Step 3 of alternative billing flow.
|
|
223
|
+
* Must be called AFTER successful payment in your payment system.
|
|
224
|
+
* Token must be reported to Google Play backend within 24 hours.
|
|
225
|
+
*
|
|
226
|
+
* Returns token string, or null if creation failed.
|
|
227
|
+
* Throws OpenIapError.NotPrepared if billing client not ready.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} sku - The product SKU that was purchased
|
|
230
|
+
* @returns {Promise<string | null>}
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```typescript
|
|
234
|
+
* const token = await createAlternativeBillingTokenAndroid('premium_subscription');
|
|
235
|
+
* if (token) {
|
|
236
|
+
* // Send token to your backend
|
|
237
|
+
* await fetch('/api/report-transaction', {
|
|
238
|
+
* method: 'POST',
|
|
239
|
+
* body: JSON.stringify({ token, sku: 'premium_subscription' })
|
|
240
|
+
* });
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export const createAlternativeBillingTokenAndroid: MutationField<
|
|
245
|
+
'createAlternativeBillingTokenAndroid'
|
|
246
|
+
> = async (sku?: string) => {
|
|
247
|
+
return ExpoIapModule.createAlternativeBillingTokenAndroid(sku);
|
|
248
|
+
};
|