react-native-iap 14.4.11 → 14.4.13
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/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +159 -10
- package/ios/HybridRnIap.swift +115 -2
- package/lib/module/hooks/useIAP.js +24 -3
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/index.js +275 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +18 -0
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/plugin/src/withIAP.d.ts +27 -0
- package/lib/typescript/plugin/src/withIAP.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +6 -1
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +133 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/RnIap.nitro.d.ts +72 -2
- package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +116 -13
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroIapOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JAlternativeBillingModeAndroid.hpp +62 -0
- package/nitrogen/generated/android/c++/JExternalPurchaseLinkResultIOS.hpp +58 -0
- package/nitrogen/generated/android/c++/JExternalPurchaseNoticeAction.hpp +59 -0
- package/nitrogen/generated/android/c++/JExternalPurchaseNoticeResultIOS.hpp +60 -0
- package/nitrogen/generated/android/c++/JFunc_void_UserChoiceBillingDetails.hpp +78 -0
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +132 -3
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +9 -1
- package/nitrogen/generated/android/c++/JInitConnectionConfig.hpp +55 -0
- package/nitrogen/generated/android/c++/JPurchaseAndroid.hpp +4 -0
- package/nitrogen/generated/android/c++/JPurchaseIOS.hpp +4 -0
- package/nitrogen/generated/android/c++/JUserChoiceBillingDetails.hpp +75 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AlternativeBillingModeAndroid.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/ExternalPurchaseLinkResultIOS.kt +32 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/ExternalPurchaseNoticeAction.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/ExternalPurchaseNoticeResultIOS.kt +32 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Func_void_UserChoiceBillingDetails.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +43 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/InitConnectionConfig.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseAndroid.kt +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseIOS.kt +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/UserChoiceBillingDetails.kt +32 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.cpp +24 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +156 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +18 -0
- package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +81 -3
- package/nitrogen/generated/ios/swift/AlternativeBillingModeAndroid.swift +44 -0
- package/nitrogen/generated/ios/swift/ExternalPurchaseLinkResultIOS.swift +65 -0
- package/nitrogen/generated/ios/swift/ExternalPurchaseNoticeAction.swift +40 -0
- package/nitrogen/generated/ios/swift/ExternalPurchaseNoticeResultIOS.swift +65 -0
- package/nitrogen/generated/ios/swift/Func_void_ExternalPurchaseLinkResultIOS.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_ExternalPurchaseNoticeResultIOS.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_UserChoiceBillingDetails.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +9 -1
- package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +168 -2
- package/nitrogen/generated/ios/swift/InitConnectionConfig.swift +47 -0
- package/nitrogen/generated/ios/swift/PurchaseAndroid.swift +31 -1
- package/nitrogen/generated/ios/swift/PurchaseIOS.swift +31 -1
- package/nitrogen/generated/ios/swift/UserChoiceBillingDetails.swift +58 -0
- package/nitrogen/generated/shared/c++/AlternativeBillingModeAndroid.hpp +80 -0
- package/nitrogen/generated/shared/c++/ExternalPurchaseLinkResultIOS.hpp +72 -0
- package/nitrogen/generated/shared/c++/ExternalPurchaseNoticeAction.hpp +76 -0
- package/nitrogen/generated/shared/c++/ExternalPurchaseNoticeResultIOS.hpp +74 -0
- package/nitrogen/generated/shared/c++/HybridRnIapSpec.cpp +8 -0
- package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +22 -2
- package/nitrogen/generated/shared/c++/InitConnectionConfig.hpp +69 -0
- package/nitrogen/generated/shared/c++/PurchaseAndroid.hpp +5 -1
- package/nitrogen/generated/shared/c++/PurchaseIOS.hpp +5 -1
- package/nitrogen/generated/shared/c++/UserChoiceBillingDetails.hpp +72 -0
- package/openiap-versions.json +3 -3
- package/package.json +1 -1
- package/plugin/build/withIAP.d.ts +27 -0
- package/plugin/build/withIAP.js +91 -1
- package/plugin/src/withIAP.ts +162 -0
- package/src/hooks/useIAP.ts +47 -1
- package/src/index.ts +313 -2
- package/src/specs/RnIap.nitro.ts +99 -1
- package/src/types.ts +124 -13
package/plugin/src/withIAP.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
withAndroidManifest,
|
|
5
5
|
withAppBuildGradle,
|
|
6
6
|
withPodfile,
|
|
7
|
+
withEntitlementsPlist,
|
|
8
|
+
withInfoPlist,
|
|
7
9
|
} from 'expo/config-plugins';
|
|
8
10
|
import type {ConfigPlugin} from 'expo/config-plugins';
|
|
9
11
|
import type {ExpoConfig} from '@expo/config-types';
|
|
@@ -165,6 +167,154 @@ const withIapAndroid: ConfigPlugin = (config) => {
|
|
|
165
167
|
return config;
|
|
166
168
|
};
|
|
167
169
|
|
|
170
|
+
export interface IosAlternativeBillingConfig {
|
|
171
|
+
/** Country codes where external purchases are supported (ISO 3166-1 alpha-2) */
|
|
172
|
+
countries?: string[];
|
|
173
|
+
/** External purchase URLs per country (iOS 15.4+) */
|
|
174
|
+
links?: Record<string, string>;
|
|
175
|
+
/** Multiple external purchase URLs per country (iOS 17.5+, up to 5 per country) */
|
|
176
|
+
multiLinks?: Record<string, string[]>;
|
|
177
|
+
/** Custom link regions (iOS 18.1+) */
|
|
178
|
+
customLinkRegions?: string[];
|
|
179
|
+
/** Streaming link regions for music apps (iOS 18.2+) */
|
|
180
|
+
streamingLinkRegions?: string[];
|
|
181
|
+
/** Enable external purchase link entitlement */
|
|
182
|
+
enableExternalPurchaseLink?: boolean;
|
|
183
|
+
/** Enable external purchase link streaming entitlement (music apps only) */
|
|
184
|
+
enableExternalPurchaseLinkStreaming?: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Add external purchase entitlements and Info.plist configuration */
|
|
188
|
+
const withIosAlternativeBilling: ConfigPlugin<
|
|
189
|
+
IosAlternativeBillingConfig | undefined
|
|
190
|
+
> = (config, options) => {
|
|
191
|
+
if (!options || !options.countries || options.countries.length === 0) {
|
|
192
|
+
return config;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Add entitlements
|
|
196
|
+
config = withEntitlementsPlist(config, (config) => {
|
|
197
|
+
// Always add basic external purchase entitlement when countries are specified
|
|
198
|
+
config.modResults['com.apple.developer.storekit.external-purchase'] = true;
|
|
199
|
+
if (!hasLoggedPluginExecution) {
|
|
200
|
+
console.log(
|
|
201
|
+
'✅ Added com.apple.developer.storekit.external-purchase to entitlements',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Add external purchase link entitlement if enabled
|
|
206
|
+
if (options.enableExternalPurchaseLink) {
|
|
207
|
+
config.modResults['com.apple.developer.storekit.external-purchase-link'] =
|
|
208
|
+
true;
|
|
209
|
+
if (!hasLoggedPluginExecution) {
|
|
210
|
+
console.log(
|
|
211
|
+
'✅ Added com.apple.developer.storekit.external-purchase-link to entitlements',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Add streaming entitlement if enabled
|
|
217
|
+
if (options.enableExternalPurchaseLinkStreaming) {
|
|
218
|
+
config.modResults[
|
|
219
|
+
'com.apple.developer.storekit.external-purchase-link-streaming'
|
|
220
|
+
] = true;
|
|
221
|
+
if (!hasLoggedPluginExecution) {
|
|
222
|
+
console.log(
|
|
223
|
+
'✅ Added com.apple.developer.storekit.external-purchase-link-streaming to entitlements',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return config;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Add Info.plist configuration
|
|
232
|
+
config = withInfoPlist(config, (config) => {
|
|
233
|
+
const plist = config.modResults;
|
|
234
|
+
|
|
235
|
+
// Helper function to normalize country codes to uppercase
|
|
236
|
+
const normalize = (code: string) => code.trim().toUpperCase();
|
|
237
|
+
|
|
238
|
+
// 1. SKExternalPurchase (Required)
|
|
239
|
+
const normalizedCountries = options.countries?.map(normalize);
|
|
240
|
+
plist.SKExternalPurchase = normalizedCountries;
|
|
241
|
+
if (!hasLoggedPluginExecution) {
|
|
242
|
+
console.log(
|
|
243
|
+
`✅ Added SKExternalPurchase with countries: ${normalizedCountries?.join(
|
|
244
|
+
', ',
|
|
245
|
+
)}`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 2. SKExternalPurchaseLink (Optional - iOS 15.4+)
|
|
250
|
+
if (options.links && Object.keys(options.links).length > 0) {
|
|
251
|
+
plist.SKExternalPurchaseLink = Object.fromEntries(
|
|
252
|
+
Object.entries(options.links).map(([code, url]) => [
|
|
253
|
+
normalize(code),
|
|
254
|
+
url,
|
|
255
|
+
]),
|
|
256
|
+
);
|
|
257
|
+
if (!hasLoggedPluginExecution) {
|
|
258
|
+
console.log(
|
|
259
|
+
`✅ Added SKExternalPurchaseLink for ${
|
|
260
|
+
Object.keys(options.links).length
|
|
261
|
+
} countries`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 3. SKExternalPurchaseMultiLink (iOS 17.5+)
|
|
267
|
+
if (options.multiLinks && Object.keys(options.multiLinks).length > 0) {
|
|
268
|
+
plist.SKExternalPurchaseMultiLink = Object.fromEntries(
|
|
269
|
+
Object.entries(options.multiLinks).map(([code, urls]) => [
|
|
270
|
+
normalize(code),
|
|
271
|
+
urls,
|
|
272
|
+
]),
|
|
273
|
+
);
|
|
274
|
+
if (!hasLoggedPluginExecution) {
|
|
275
|
+
console.log(
|
|
276
|
+
`✅ Added SKExternalPurchaseMultiLink for ${
|
|
277
|
+
Object.keys(options.multiLinks).length
|
|
278
|
+
} countries`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 4. SKExternalPurchaseCustomLinkRegions (iOS 18.1+)
|
|
284
|
+
if (options.customLinkRegions && options.customLinkRegions.length > 0) {
|
|
285
|
+
plist.SKExternalPurchaseCustomLinkRegions =
|
|
286
|
+
options.customLinkRegions.map(normalize);
|
|
287
|
+
if (!hasLoggedPluginExecution) {
|
|
288
|
+
console.log(
|
|
289
|
+
`✅ Added SKExternalPurchaseCustomLinkRegions: ${options.customLinkRegions
|
|
290
|
+
.map(normalize)
|
|
291
|
+
.join(', ')}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 5. SKExternalPurchaseLinkStreamingRegions (iOS 18.2+)
|
|
297
|
+
if (
|
|
298
|
+
options.streamingLinkRegions &&
|
|
299
|
+
options.streamingLinkRegions.length > 0
|
|
300
|
+
) {
|
|
301
|
+
plist.SKExternalPurchaseLinkStreamingRegions =
|
|
302
|
+
options.streamingLinkRegions.map(normalize);
|
|
303
|
+
if (!hasLoggedPluginExecution) {
|
|
304
|
+
console.log(
|
|
305
|
+
`✅ Added SKExternalPurchaseLinkStreamingRegions: ${options.streamingLinkRegions
|
|
306
|
+
.map(normalize)
|
|
307
|
+
.join(', ')}`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return config;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return config;
|
|
316
|
+
};
|
|
317
|
+
|
|
168
318
|
type IapPluginProps = {
|
|
169
319
|
ios?: {
|
|
170
320
|
// Enable to inject Folly coroutine-disabling macros into Podfile during prebuild
|
|
@@ -172,6 +322,13 @@ type IapPluginProps = {
|
|
|
172
322
|
// @deprecated Use 'with-folly-no-coroutines'. Kept for backward compatibility.
|
|
173
323
|
'with-folly-no-couroutines'?: boolean;
|
|
174
324
|
};
|
|
325
|
+
/**
|
|
326
|
+
* iOS Alternative Billing configuration.
|
|
327
|
+
* Configure external purchase countries, links, and entitlements.
|
|
328
|
+
* Requires approval from Apple.
|
|
329
|
+
* @platform ios
|
|
330
|
+
*/
|
|
331
|
+
iosAlternativeBilling?: IosAlternativeBillingConfig;
|
|
175
332
|
};
|
|
176
333
|
|
|
177
334
|
const withIapIosFollyWorkaround: ConfigPlugin<IapPluginProps | undefined> = (
|
|
@@ -236,6 +393,10 @@ const withIAP: ConfigPlugin<IapPluginProps | undefined> = (config, props) => {
|
|
|
236
393
|
try {
|
|
237
394
|
let result = withIapAndroid(config);
|
|
238
395
|
result = withIapIosFollyWorkaround(result, props);
|
|
396
|
+
// Add iOS alternative billing configuration if provided
|
|
397
|
+
if (props?.iosAlternativeBilling) {
|
|
398
|
+
result = withIosAlternativeBilling(result, props.iosAlternativeBilling);
|
|
399
|
+
}
|
|
239
400
|
// Set flag after first execution to prevent duplicate logs
|
|
240
401
|
hasLoggedPluginExecution = true;
|
|
241
402
|
return result;
|
|
@@ -270,4 +431,5 @@ const pluginExport: IapPluginCallable = ((
|
|
|
270
431
|
props?: IapPluginProps,
|
|
271
432
|
) => _wrapped(config, props)) as unknown as IapPluginCallable;
|
|
272
433
|
|
|
434
|
+
export {withIosAlternativeBilling};
|
|
273
435
|
export {pluginExport as default};
|
package/src/hooks/useIAP.ts
CHANGED
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
restorePurchases as restorePurchasesTopLevel,
|
|
20
20
|
getPromotedProductIOS,
|
|
21
21
|
requestPurchaseOnPromotedProductIOS,
|
|
22
|
+
checkAlternativeBillingAvailabilityAndroid,
|
|
23
|
+
showAlternativeBillingDialogAndroid,
|
|
24
|
+
createAlternativeBillingTokenAndroid,
|
|
25
|
+
userChoiceBillingListenerAndroid,
|
|
22
26
|
} from '../';
|
|
23
27
|
|
|
24
28
|
// Types
|
|
@@ -27,6 +31,8 @@ import type {
|
|
|
27
31
|
ProductQueryType,
|
|
28
32
|
RequestPurchaseProps,
|
|
29
33
|
RequestPurchaseResult,
|
|
34
|
+
AlternativeBillingModeAndroid,
|
|
35
|
+
UserChoiceBillingDetails,
|
|
30
36
|
} from '../types';
|
|
31
37
|
import type {
|
|
32
38
|
ActiveSubscription,
|
|
@@ -75,12 +81,20 @@ type UseIap = {
|
|
|
75
81
|
subscriptionIds?: string[],
|
|
76
82
|
) => Promise<ActiveSubscription[]>;
|
|
77
83
|
hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
|
|
84
|
+
// Alternative billing (Android)
|
|
85
|
+
checkAlternativeBillingAvailabilityAndroid?: () => Promise<boolean>;
|
|
86
|
+
showAlternativeBillingDialogAndroid?: () => Promise<boolean>;
|
|
87
|
+
createAlternativeBillingTokenAndroid?: (
|
|
88
|
+
sku?: string,
|
|
89
|
+
) => Promise<string | null>;
|
|
78
90
|
};
|
|
79
91
|
|
|
80
92
|
export interface UseIapOptions {
|
|
81
93
|
onPurchaseSuccess?: (purchase: Purchase) => void;
|
|
82
94
|
onPurchaseError?: (error: PurchaseError) => void;
|
|
83
95
|
onPromotedProductIOS?: (product: Product) => void;
|
|
96
|
+
onUserChoiceBillingAndroid?: (details: UserChoiceBillingDetails) => void;
|
|
97
|
+
alternativeBillingModeAndroid?: AlternativeBillingModeAndroid;
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
/**
|
|
@@ -133,6 +147,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
133
147
|
purchaseUpdate?: EventSubscription;
|
|
134
148
|
purchaseError?: EventSubscription;
|
|
135
149
|
promotedProductIOS?: EventSubscription;
|
|
150
|
+
userChoiceBillingAndroid?: EventSubscription;
|
|
136
151
|
}>({});
|
|
137
152
|
|
|
138
153
|
const subscriptionsRefState = useRef<ProductSubscription[]>([]);
|
|
@@ -346,7 +361,29 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
346
361
|
);
|
|
347
362
|
}
|
|
348
363
|
|
|
349
|
-
|
|
364
|
+
// Add user choice billing listener for Android (if provided)
|
|
365
|
+
if (
|
|
366
|
+
Platform.OS === 'android' &&
|
|
367
|
+
optionsRef.current?.onUserChoiceBillingAndroid
|
|
368
|
+
) {
|
|
369
|
+
subscriptionsRef.current.userChoiceBillingAndroid =
|
|
370
|
+
userChoiceBillingListenerAndroid((details) => {
|
|
371
|
+
if (optionsRef.current?.onUserChoiceBillingAndroid) {
|
|
372
|
+
optionsRef.current.onUserChoiceBillingAndroid(details);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Initialize connection with config
|
|
378
|
+
const config =
|
|
379
|
+
Platform.OS === 'android' &&
|
|
380
|
+
optionsRef.current?.alternativeBillingModeAndroid
|
|
381
|
+
? {
|
|
382
|
+
alternativeBillingModeAndroid:
|
|
383
|
+
optionsRef.current.alternativeBillingModeAndroid,
|
|
384
|
+
}
|
|
385
|
+
: undefined;
|
|
386
|
+
const result = await initConnection(config);
|
|
350
387
|
setConnected(result);
|
|
351
388
|
if (!result) {
|
|
352
389
|
// Clean up some listeners but leave purchaseError for potential retries
|
|
@@ -364,6 +401,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
364
401
|
currentSubscriptions.purchaseUpdate?.remove();
|
|
365
402
|
currentSubscriptions.purchaseError?.remove();
|
|
366
403
|
currentSubscriptions.promotedProductIOS?.remove();
|
|
404
|
+
currentSubscriptions.userChoiceBillingAndroid?.remove();
|
|
367
405
|
// Keep connection alive across screens to avoid race conditions
|
|
368
406
|
setConnected(false);
|
|
369
407
|
};
|
|
@@ -393,5 +431,13 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
393
431
|
requestPurchaseOnPromotedProductIOS,
|
|
394
432
|
getActiveSubscriptions: getActiveSubscriptionsInternal,
|
|
395
433
|
hasActiveSubscriptions: hasActiveSubscriptionsInternal,
|
|
434
|
+
// Alternative billing (Android only)
|
|
435
|
+
...(Platform.OS === 'android'
|
|
436
|
+
? {
|
|
437
|
+
checkAlternativeBillingAvailabilityAndroid,
|
|
438
|
+
showAlternativeBillingDialogAndroid,
|
|
439
|
+
createAlternativeBillingTokenAndroid,
|
|
440
|
+
}
|
|
441
|
+
: {}),
|
|
396
442
|
};
|
|
397
443
|
}
|
package/src/index.ts
CHANGED
|
@@ -292,6 +292,82 @@ export const promotedProductListenerIOS = (
|
|
|
292
292
|
};
|
|
293
293
|
};
|
|
294
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Add a listener for user choice billing events (Android only).
|
|
297
|
+
* Fires when a user selects alternative billing in the User Choice Billing dialog.
|
|
298
|
+
*
|
|
299
|
+
* @param listener - Function to call when user chooses alternative billing
|
|
300
|
+
* @returns EventSubscription with remove() method to unsubscribe
|
|
301
|
+
* @platform Android
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```typescript
|
|
305
|
+
* const subscription = userChoiceBillingListenerAndroid((details) => {
|
|
306
|
+
* console.log('User chose alternative billing');
|
|
307
|
+
* console.log('Products:', details.products);
|
|
308
|
+
* console.log('Token:', details.externalTransactionToken);
|
|
309
|
+
*
|
|
310
|
+
* // Send token to backend for Google Play reporting
|
|
311
|
+
* await reportToGooglePlay(details.externalTransactionToken);
|
|
312
|
+
* });
|
|
313
|
+
*
|
|
314
|
+
* // Later, remove the listener
|
|
315
|
+
* subscription.remove();
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
type NitroUserChoiceBillingListener = Parameters<
|
|
319
|
+
RnIap['addUserChoiceBillingListenerAndroid']
|
|
320
|
+
>[0];
|
|
321
|
+
const userChoiceBillingListenerMap = new WeakMap<
|
|
322
|
+
(details: any) => void,
|
|
323
|
+
NitroUserChoiceBillingListener
|
|
324
|
+
>();
|
|
325
|
+
|
|
326
|
+
export const userChoiceBillingListenerAndroid = (
|
|
327
|
+
listener: (details: any) => void,
|
|
328
|
+
): EventSubscription => {
|
|
329
|
+
if (Platform.OS !== 'android') {
|
|
330
|
+
RnIapConsole.warn(
|
|
331
|
+
'userChoiceBillingListenerAndroid: This listener is only available on Android',
|
|
332
|
+
);
|
|
333
|
+
return {remove: () => {}};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const wrappedListener: NitroUserChoiceBillingListener = (details) => {
|
|
337
|
+
listener(details);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
userChoiceBillingListenerMap.set(listener, wrappedListener);
|
|
341
|
+
let attached = false;
|
|
342
|
+
try {
|
|
343
|
+
IAP.instance.addUserChoiceBillingListenerAndroid(wrappedListener);
|
|
344
|
+
attached = true;
|
|
345
|
+
} catch (e) {
|
|
346
|
+
const msg = toErrorMessage(e);
|
|
347
|
+
if (msg.includes('Nitro runtime not installed')) {
|
|
348
|
+
RnIapConsole.warn(
|
|
349
|
+
'[userChoiceBillingListenerAndroid] Nitro not ready yet; listener inert until initConnection()',
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
throw e;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
remove: () => {
|
|
358
|
+
const wrapped = userChoiceBillingListenerMap.get(listener);
|
|
359
|
+
if (wrapped) {
|
|
360
|
+
if (attached) {
|
|
361
|
+
try {
|
|
362
|
+
IAP.instance.removeUserChoiceBillingListenerAndroid(wrapped);
|
|
363
|
+
} catch {}
|
|
364
|
+
}
|
|
365
|
+
userChoiceBillingListenerMap.delete(listener);
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
|
|
295
371
|
// ------------------------------
|
|
296
372
|
// Query API
|
|
297
373
|
// ------------------------------
|
|
@@ -832,10 +908,32 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
|
|
|
832
908
|
|
|
833
909
|
/**
|
|
834
910
|
* Initialize connection to the store
|
|
911
|
+
* @param config - Optional configuration including alternative billing mode for Android
|
|
912
|
+
* @param config.alternativeBillingModeAndroid - Alternative billing mode: 'none', 'user-choice', or 'alternative-only'
|
|
913
|
+
*
|
|
914
|
+
* @example
|
|
915
|
+
* ```typescript
|
|
916
|
+
* // Standard billing (default)
|
|
917
|
+
* await initConnection();
|
|
918
|
+
*
|
|
919
|
+
* // User choice billing (Android)
|
|
920
|
+
* await initConnection({
|
|
921
|
+
* alternativeBillingModeAndroid: 'user-choice'
|
|
922
|
+
* });
|
|
923
|
+
*
|
|
924
|
+
* // Alternative billing only (Android)
|
|
925
|
+
* await initConnection({
|
|
926
|
+
* alternativeBillingModeAndroid: 'alternative-only'
|
|
927
|
+
* });
|
|
928
|
+
* ```
|
|
835
929
|
*/
|
|
836
|
-
export const initConnection: MutationField<'initConnection'> = async (
|
|
930
|
+
export const initConnection: MutationField<'initConnection'> = async (
|
|
931
|
+
config,
|
|
932
|
+
) => {
|
|
837
933
|
try {
|
|
838
|
-
return await IAP.instance.initConnection(
|
|
934
|
+
return await IAP.instance.initConnection(
|
|
935
|
+
config as Record<string, unknown> | undefined,
|
|
936
|
+
);
|
|
839
937
|
} catch (error) {
|
|
840
938
|
RnIapConsole.error('Failed to initialize IAP connection:', error);
|
|
841
939
|
throw error;
|
|
@@ -1648,3 +1746,216 @@ const normalizeProductQueryType = (
|
|
|
1648
1746
|
}
|
|
1649
1747
|
return 'in-app';
|
|
1650
1748
|
};
|
|
1749
|
+
|
|
1750
|
+
// ============================================================================
|
|
1751
|
+
// ALTERNATIVE BILLING APIs
|
|
1752
|
+
// ============================================================================
|
|
1753
|
+
|
|
1754
|
+
// ------------------------------
|
|
1755
|
+
// Android Alternative Billing
|
|
1756
|
+
// ------------------------------
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* Check if alternative billing is available for this user/device (Android only).
|
|
1760
|
+
* Step 1 of alternative billing flow.
|
|
1761
|
+
*
|
|
1762
|
+
* @returns Promise<boolean> - true if available, false otherwise
|
|
1763
|
+
* @throws Error if billing client not ready
|
|
1764
|
+
* @platform Android
|
|
1765
|
+
*
|
|
1766
|
+
* @example
|
|
1767
|
+
* ```typescript
|
|
1768
|
+
* const isAvailable = await checkAlternativeBillingAvailabilityAndroid();
|
|
1769
|
+
* if (isAvailable) {
|
|
1770
|
+
* // Proceed with alternative billing flow
|
|
1771
|
+
* }
|
|
1772
|
+
* ```
|
|
1773
|
+
*/
|
|
1774
|
+
export const checkAlternativeBillingAvailabilityAndroid: MutationField<
|
|
1775
|
+
'checkAlternativeBillingAvailabilityAndroid'
|
|
1776
|
+
> = async () => {
|
|
1777
|
+
if (Platform.OS !== 'android') {
|
|
1778
|
+
throw new Error('Alternative billing is only supported on Android');
|
|
1779
|
+
}
|
|
1780
|
+
try {
|
|
1781
|
+
return await IAP.instance.checkAlternativeBillingAvailabilityAndroid();
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
RnIapConsole.error(
|
|
1784
|
+
'Failed to check alternative billing availability:',
|
|
1785
|
+
error,
|
|
1786
|
+
);
|
|
1787
|
+
throw error;
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Show alternative billing information dialog to user (Android only).
|
|
1793
|
+
* Step 2 of alternative billing flow.
|
|
1794
|
+
* Must be called BEFORE processing payment in your payment system.
|
|
1795
|
+
*
|
|
1796
|
+
* @returns Promise<boolean> - true if user accepted, false if user canceled
|
|
1797
|
+
* @throws Error if billing client not ready
|
|
1798
|
+
* @platform Android
|
|
1799
|
+
*
|
|
1800
|
+
* @example
|
|
1801
|
+
* ```typescript
|
|
1802
|
+
* const userAccepted = await showAlternativeBillingDialogAndroid();
|
|
1803
|
+
* if (userAccepted) {
|
|
1804
|
+
* // Process payment in your payment system
|
|
1805
|
+
* const success = await processCustomPayment();
|
|
1806
|
+
* if (success) {
|
|
1807
|
+
* // Create reporting token
|
|
1808
|
+
* const token = await createAlternativeBillingTokenAndroid();
|
|
1809
|
+
* // Send token to your backend for Google Play reporting
|
|
1810
|
+
* }
|
|
1811
|
+
* }
|
|
1812
|
+
* ```
|
|
1813
|
+
*/
|
|
1814
|
+
export const showAlternativeBillingDialogAndroid: MutationField<
|
|
1815
|
+
'showAlternativeBillingDialogAndroid'
|
|
1816
|
+
> = async () => {
|
|
1817
|
+
if (Platform.OS !== 'android') {
|
|
1818
|
+
throw new Error('Alternative billing is only supported on Android');
|
|
1819
|
+
}
|
|
1820
|
+
try {
|
|
1821
|
+
return await IAP.instance.showAlternativeBillingDialogAndroid();
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
RnIapConsole.error('Failed to show alternative billing dialog:', error);
|
|
1824
|
+
throw error;
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Create external transaction token for Google Play reporting (Android only).
|
|
1830
|
+
* Step 3 of alternative billing flow.
|
|
1831
|
+
* Must be called AFTER successful payment in your payment system.
|
|
1832
|
+
* Token must be reported to Google Play backend within 24 hours.
|
|
1833
|
+
*
|
|
1834
|
+
* @param sku - Optional product SKU that was purchased
|
|
1835
|
+
* @returns Promise<string | null> - Token string or null if creation failed
|
|
1836
|
+
* @throws Error if billing client not ready
|
|
1837
|
+
* @platform Android
|
|
1838
|
+
*
|
|
1839
|
+
* @example
|
|
1840
|
+
* ```typescript
|
|
1841
|
+
* const token = await createAlternativeBillingTokenAndroid('premium_subscription');
|
|
1842
|
+
* if (token) {
|
|
1843
|
+
* // Send token to your backend
|
|
1844
|
+
* await fetch('/api/report-transaction', {
|
|
1845
|
+
* method: 'POST',
|
|
1846
|
+
* body: JSON.stringify({ token, sku: 'premium_subscription' })
|
|
1847
|
+
* });
|
|
1848
|
+
* }
|
|
1849
|
+
* ```
|
|
1850
|
+
*/
|
|
1851
|
+
export const createAlternativeBillingTokenAndroid: MutationField<
|
|
1852
|
+
'createAlternativeBillingTokenAndroid'
|
|
1853
|
+
> = async (sku?: string) => {
|
|
1854
|
+
if (Platform.OS !== 'android') {
|
|
1855
|
+
throw new Error('Alternative billing is only supported on Android');
|
|
1856
|
+
}
|
|
1857
|
+
try {
|
|
1858
|
+
return await IAP.instance.createAlternativeBillingTokenAndroid(sku ?? null);
|
|
1859
|
+
} catch (error) {
|
|
1860
|
+
RnIapConsole.error('Failed to create alternative billing token:', error);
|
|
1861
|
+
throw error;
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
// ------------------------------
|
|
1866
|
+
// iOS External Purchase
|
|
1867
|
+
// ------------------------------
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Check if the device can present an external purchase notice sheet (iOS 18.2+).
|
|
1871
|
+
*
|
|
1872
|
+
* @returns Promise<boolean> - true if notice sheet can be presented
|
|
1873
|
+
* @platform iOS
|
|
1874
|
+
*
|
|
1875
|
+
* @example
|
|
1876
|
+
* ```typescript
|
|
1877
|
+
* const canPresent = await canPresentExternalPurchaseNoticeIOS();
|
|
1878
|
+
* if (canPresent) {
|
|
1879
|
+
* // Present notice before external purchase
|
|
1880
|
+
* const result = await presentExternalPurchaseNoticeSheetIOS();
|
|
1881
|
+
* }
|
|
1882
|
+
* ```
|
|
1883
|
+
*/
|
|
1884
|
+
export const canPresentExternalPurchaseNoticeIOS: QueryField<
|
|
1885
|
+
'canPresentExternalPurchaseNoticeIOS'
|
|
1886
|
+
> = async () => {
|
|
1887
|
+
if (Platform.OS !== 'ios') {
|
|
1888
|
+
return false;
|
|
1889
|
+
}
|
|
1890
|
+
try {
|
|
1891
|
+
return await IAP.instance.canPresentExternalPurchaseNoticeIOS();
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
RnIapConsole.error(
|
|
1894
|
+
'Failed to check external purchase notice availability:',
|
|
1895
|
+
error,
|
|
1896
|
+
);
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
};
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* Present an external purchase notice sheet to inform users about external purchases (iOS 18.2+).
|
|
1903
|
+
* This must be called before opening an external purchase link.
|
|
1904
|
+
*
|
|
1905
|
+
* @returns Promise<ExternalPurchaseNoticeResultIOS> - Result with action and error if any
|
|
1906
|
+
* @platform iOS
|
|
1907
|
+
*
|
|
1908
|
+
* @example
|
|
1909
|
+
* ```typescript
|
|
1910
|
+
* const result = await presentExternalPurchaseNoticeSheetIOS();
|
|
1911
|
+
* if (result.result === 'continue') {
|
|
1912
|
+
* // User chose to continue, open external purchase link
|
|
1913
|
+
* await presentExternalPurchaseLinkIOS('https://your-website.com/purchase');
|
|
1914
|
+
* }
|
|
1915
|
+
* ```
|
|
1916
|
+
*/
|
|
1917
|
+
export const presentExternalPurchaseNoticeSheetIOS: MutationField<
|
|
1918
|
+
'presentExternalPurchaseNoticeSheetIOS'
|
|
1919
|
+
> = async () => {
|
|
1920
|
+
if (Platform.OS !== 'ios') {
|
|
1921
|
+
throw new Error('External purchase is only supported on iOS');
|
|
1922
|
+
}
|
|
1923
|
+
try {
|
|
1924
|
+
return (await IAP.instance.presentExternalPurchaseNoticeSheetIOS()) as any;
|
|
1925
|
+
} catch (error) {
|
|
1926
|
+
RnIapConsole.error(
|
|
1927
|
+
'Failed to present external purchase notice sheet:',
|
|
1928
|
+
error,
|
|
1929
|
+
);
|
|
1930
|
+
throw error;
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
|
|
1934
|
+
/**
|
|
1935
|
+
* Present an external purchase link to redirect users to your website (iOS 16.0+).
|
|
1936
|
+
*
|
|
1937
|
+
* @param url - The external purchase URL to open
|
|
1938
|
+
* @returns Promise<ExternalPurchaseLinkResultIOS> - Result with success status and error if any
|
|
1939
|
+
* @platform iOS
|
|
1940
|
+
*
|
|
1941
|
+
* @example
|
|
1942
|
+
* ```typescript
|
|
1943
|
+
* const result = await presentExternalPurchaseLinkIOS('https://your-website.com/purchase');
|
|
1944
|
+
* if (result.success) {
|
|
1945
|
+
* console.log('User completed external purchase');
|
|
1946
|
+
* }
|
|
1947
|
+
* ```
|
|
1948
|
+
*/
|
|
1949
|
+
export const presentExternalPurchaseLinkIOS: MutationField<
|
|
1950
|
+
'presentExternalPurchaseLinkIOS'
|
|
1951
|
+
> = async (url) => {
|
|
1952
|
+
if (Platform.OS !== 'ios') {
|
|
1953
|
+
throw new Error('External purchase is only supported on iOS');
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
return (await IAP.instance.presentExternalPurchaseLinkIOS(url)) as any;
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
RnIapConsole.error('Failed to present external purchase link:', error);
|
|
1959
|
+
throw error;
|
|
1960
|
+
}
|
|
1961
|
+
};
|