react-native-iap 16.4.1 → 16.5.0
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 +200 -142
- package/android/src/main/java/com/margelo/nitro/iap/TokenizedListenerRegistry.kt +0 -1
- package/ios/HybridRnIap.swift +832 -165
- package/ios/RnIapHelper.swift +1 -1
- package/lib/module/hooks/useIAP.js +48 -32
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/index.js +206 -152
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.kepler.js +1 -0
- package/lib/module/index.kepler.js.map +1 -1
- package/lib/module/types.js +1 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils/error.js +5 -4
- package/lib/module/utils/error.js.map +1 -1
- package/lib/module/vega-adapter.js +4 -1
- package/lib/module/vega-adapter.js.map +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +31 -15
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +28 -14
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.kepler.d.ts.map +1 -1
- package/lib/typescript/src/specs/RnIap.nitro.d.ts +10 -0
- package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +11 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/error.d.ts.map +1 -1
- package/lib/typescript/src/vega-adapter.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -0
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JNitroPurchaseRequest.hpp +7 -1
- package/nitrogen/generated/android/c++/JNitroPurchaseRequestType.hpp +58 -0
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitHorizonProps.hpp +66 -0
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitProps.hpp +8 -1
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithProviderProps.hpp +3 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.hpp +74 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitProps.hpp +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequest.kt +8 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequestType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitHorizonProps.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitProps.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.kt +62 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +65 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +6 -0
- package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +6 -0
- package/nitrogen/generated/ios/swift/NitroPurchaseRequest.swift +13 -2
- package/nitrogen/generated/ios/swift/NitroPurchaseRequestType.swift +40 -0
- package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitHorizonProps.swift +66 -0
- package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitProps.swift +38 -1
- package/nitrogen/generated/ios/swift/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.swift +30 -0
- package/nitrogen/generated/shared/c++/NitroPurchaseRequest.hpp +9 -2
- package/nitrogen/generated/shared/c++/NitroPurchaseRequestType.hpp +76 -0
- package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitHorizonProps.hpp +90 -0
- package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitProps.hpp +8 -1
- package/openiap-versions.json +3 -3
- package/package.json +1 -1
- package/src/hooks/useIAP.ts +96 -58
- package/src/index.kepler.ts +1 -0
- package/src/index.ts +248 -195
- package/src/specs/RnIap.nitro.ts +12 -0
- package/src/types.ts +12 -2
- package/src/utils/error.ts +7 -3
- package/src/vega-adapter.ts +9 -5
package/src/index.ts
CHANGED
|
@@ -170,6 +170,7 @@ export type {
|
|
|
170
170
|
|
|
171
171
|
// Create the RnIap HybridObject instance lazily to avoid early JSI crashes
|
|
172
172
|
let iapRef: RnIap | null = null;
|
|
173
|
+
let attachingPendingNativeListeners = false;
|
|
173
174
|
|
|
174
175
|
/**
|
|
175
176
|
* Check if Nitro runtime is ready for IAP operations.
|
|
@@ -220,39 +221,52 @@ const isAndroidStoreRuntime = (): boolean => {
|
|
|
220
221
|
return Platform.OS === 'android' || isVegaOS();
|
|
221
222
|
};
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (iapRef) return iapRef;
|
|
224
|
+
function getRawIapInstance(): RnIap {
|
|
225
|
+
if (iapRef) return iapRef;
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
iapRef = vegaModule;
|
|
235
|
-
return iapRef;
|
|
227
|
+
if (isVegaOS()) {
|
|
228
|
+
const vegaModule = getVegaIapModule();
|
|
229
|
+
if (!vegaModule) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
'Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.',
|
|
232
|
+
);
|
|
236
233
|
}
|
|
234
|
+
iapRef = vegaModule;
|
|
235
|
+
return iapRef;
|
|
236
|
+
}
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
238
|
+
// Attempt to create the HybridObject and map common Nitro/JSI readiness errors
|
|
239
|
+
try {
|
|
240
|
+
iapRef = NitroModules.createHybridObject<RnIap>('RnIap');
|
|
241
|
+
} catch (e) {
|
|
242
|
+
const msg = toErrorMessage(e);
|
|
243
|
+
if (
|
|
244
|
+
msg.includes('Nitro') ||
|
|
245
|
+
msg.includes('JSI') ||
|
|
246
|
+
msg.includes('dispatcher') ||
|
|
247
|
+
msg.includes('HybridObject')
|
|
248
|
+
) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
'Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.',
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
return iapRef;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const IAP = {
|
|
259
|
+
get instance(): RnIap {
|
|
260
|
+
const instance = getRawIapInstance();
|
|
261
|
+
if (!attachingPendingNativeListeners) {
|
|
262
|
+
attachingPendingNativeListeners = true;
|
|
263
|
+
try {
|
|
264
|
+
tryAttachPendingNativeListeners();
|
|
265
|
+
} finally {
|
|
266
|
+
attachingPendingNativeListeners = false;
|
|
252
267
|
}
|
|
253
|
-
throw e;
|
|
254
268
|
}
|
|
255
|
-
return
|
|
269
|
+
return instance;
|
|
256
270
|
},
|
|
257
271
|
};
|
|
258
272
|
|
|
@@ -264,6 +278,20 @@ const IAP = {
|
|
|
264
278
|
// hyodotdev/openiap#382.
|
|
265
279
|
// ============================================================================
|
|
266
280
|
|
|
281
|
+
function attachNativeListenerOrDefer(label: string, attach: () => void): void {
|
|
282
|
+
try {
|
|
283
|
+
attach();
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (toErrorMessage(error).includes('Nitro runtime not installed')) {
|
|
286
|
+
RnIapConsole.warn(
|
|
287
|
+
`[${label}] Nitro not ready yet; will retry on the next native operation`,
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
267
295
|
const purchaseUpdateJsListeners = new Set<(purchase: Purchase) => void>();
|
|
268
296
|
const purchaseUpdateDuplicateJsListeners = new Set<
|
|
269
297
|
(purchase: Purchase) => void
|
|
@@ -304,6 +332,38 @@ const purchaseUpdateDuplicateNativeHandler: NitroPurchaseListener = (
|
|
|
304
332
|
);
|
|
305
333
|
};
|
|
306
334
|
|
|
335
|
+
function tryAttachPurchaseUpdateNative(
|
|
336
|
+
receiveDuplicateTransactionUpdatesIOS: boolean,
|
|
337
|
+
): void {
|
|
338
|
+
const alreadyAttached = receiveDuplicateTransactionUpdatesIOS
|
|
339
|
+
? purchaseUpdateDuplicateNativeAttached
|
|
340
|
+
: purchaseUpdateNativeAttached;
|
|
341
|
+
if (alreadyAttached) return;
|
|
342
|
+
|
|
343
|
+
attachNativeListenerOrDefer('purchaseUpdatedListener', () => {
|
|
344
|
+
const nativeOptions:
|
|
345
|
+
| (NitroPurchaseUpdatedListenerOptions &
|
|
346
|
+
NitroPurchaseUpdatedListenerOptionsParam)
|
|
347
|
+
| undefined = receiveDuplicateTransactionUpdatesIOS
|
|
348
|
+
? {dedupeTransactionIOS: false}
|
|
349
|
+
: undefined;
|
|
350
|
+
const token = getRawIapInstance().addPurchaseUpdatedListener(
|
|
351
|
+
receiveDuplicateTransactionUpdatesIOS
|
|
352
|
+
? purchaseUpdateDuplicateNativeHandler
|
|
353
|
+
: purchaseUpdateNativeHandler,
|
|
354
|
+
nativeOptions,
|
|
355
|
+
);
|
|
356
|
+
if (receiveDuplicateTransactionUpdatesIOS) {
|
|
357
|
+
purchaseUpdateDuplicateNativeToken =
|
|
358
|
+
typeof token === 'number' ? token : null;
|
|
359
|
+
purchaseUpdateDuplicateNativeAttached = true;
|
|
360
|
+
} else {
|
|
361
|
+
purchaseUpdateNativeToken = typeof token === 'number' ? token : null;
|
|
362
|
+
purchaseUpdateNativeAttached = true;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
307
367
|
const purchaseErrorJsListeners = new Set<(error: PurchaseError) => void>();
|
|
308
368
|
let purchaseErrorNativeAttached = false;
|
|
309
369
|
const purchaseErrorNativeHandler: NitroPurchaseErrorListener = (error) => {
|
|
@@ -334,6 +394,14 @@ const purchaseErrorNativeHandler: NitroPurchaseErrorListener = (error) => {
|
|
|
334
394
|
}
|
|
335
395
|
};
|
|
336
396
|
|
|
397
|
+
function tryAttachPurchaseErrorNative(): void {
|
|
398
|
+
if (purchaseErrorNativeAttached) return;
|
|
399
|
+
attachNativeListenerOrDefer('purchaseErrorListener', () => {
|
|
400
|
+
getRawIapInstance().addPurchaseErrorListener(purchaseErrorNativeHandler);
|
|
401
|
+
purchaseErrorNativeAttached = true;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
337
405
|
const promotedProductJsListeners = new Set<(product: Product) => void>();
|
|
338
406
|
let promotedProductNativeAttached = false;
|
|
339
407
|
const promotedProductNativeHandler: NitroPromotedProductListener = (
|
|
@@ -356,6 +424,16 @@ const promotedProductNativeHandler: NitroPromotedProductListener = (
|
|
|
356
424
|
}
|
|
357
425
|
};
|
|
358
426
|
|
|
427
|
+
function tryAttachPromotedProductNative(): void {
|
|
428
|
+
if (promotedProductNativeAttached) return;
|
|
429
|
+
attachNativeListenerOrDefer('promotedProductListenerIOS', () => {
|
|
430
|
+
getRawIapInstance().addPromotedProductListenerIOS(
|
|
431
|
+
promotedProductNativeHandler,
|
|
432
|
+
);
|
|
433
|
+
promotedProductNativeAttached = true;
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
359
437
|
/**
|
|
360
438
|
* Reset all JS-level listener tracking state.
|
|
361
439
|
* Called during endConnection to ensure clean re-registration on next initConnection.
|
|
@@ -391,52 +469,11 @@ export const purchaseUpdatedListener = (
|
|
|
391
469
|
: purchaseUpdateJsListeners;
|
|
392
470
|
|
|
393
471
|
listeners.add(listener);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
);
|
|
400
|
-
purchaseUpdateNativeToken = typeof token === 'number' ? token : null;
|
|
401
|
-
purchaseUpdateNativeAttached = true;
|
|
402
|
-
} catch (e) {
|
|
403
|
-
const msg = toErrorMessage(e);
|
|
404
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
405
|
-
RnIapConsole.warn(
|
|
406
|
-
'[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()',
|
|
407
|
-
);
|
|
408
|
-
} else {
|
|
409
|
-
throw e;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
!purchaseUpdateDuplicateNativeAttached &&
|
|
416
|
-
receiveDuplicateTransactionUpdatesIOS
|
|
417
|
-
) {
|
|
418
|
-
try {
|
|
419
|
-
const nativeOptions: NitroPurchaseUpdatedListenerOptions &
|
|
420
|
-
NitroPurchaseUpdatedListenerOptionsParam = {
|
|
421
|
-
dedupeTransactionIOS: false,
|
|
422
|
-
};
|
|
423
|
-
const token = IAP.instance.addPurchaseUpdatedListener(
|
|
424
|
-
purchaseUpdateDuplicateNativeHandler,
|
|
425
|
-
nativeOptions,
|
|
426
|
-
);
|
|
427
|
-
purchaseUpdateDuplicateNativeToken =
|
|
428
|
-
typeof token === 'number' ? token : null;
|
|
429
|
-
purchaseUpdateDuplicateNativeAttached = true;
|
|
430
|
-
} catch (e) {
|
|
431
|
-
const msg = toErrorMessage(e);
|
|
432
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
433
|
-
RnIapConsole.warn(
|
|
434
|
-
'[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()',
|
|
435
|
-
);
|
|
436
|
-
} else {
|
|
437
|
-
throw e;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
472
|
+
try {
|
|
473
|
+
tryAttachPurchaseUpdateNative(receiveDuplicateTransactionUpdatesIOS);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
listeners.delete(listener);
|
|
476
|
+
throw error;
|
|
440
477
|
}
|
|
441
478
|
|
|
442
479
|
let removed = false;
|
|
@@ -466,7 +503,7 @@ export const purchaseUpdatedListener = (
|
|
|
466
503
|
}
|
|
467
504
|
|
|
468
505
|
try {
|
|
469
|
-
|
|
506
|
+
getRawIapInstance().removePurchaseUpdatedListener(token);
|
|
470
507
|
if (receiveDuplicateTransactionUpdatesIOS) {
|
|
471
508
|
purchaseUpdateDuplicateNativeToken = null;
|
|
472
509
|
purchaseUpdateDuplicateNativeAttached = false;
|
|
@@ -485,21 +522,11 @@ export const purchaseErrorListener = (
|
|
|
485
522
|
listener: (error: PurchaseError) => void,
|
|
486
523
|
): EventSubscription => {
|
|
487
524
|
purchaseErrorJsListeners.add(listener);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
} catch (e) {
|
|
494
|
-
const msg = toErrorMessage(e);
|
|
495
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
496
|
-
RnIapConsole.warn(
|
|
497
|
-
'[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()',
|
|
498
|
-
);
|
|
499
|
-
} else {
|
|
500
|
-
throw e;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
525
|
+
try {
|
|
526
|
+
tryAttachPurchaseErrorNative();
|
|
527
|
+
} catch (error) {
|
|
528
|
+
purchaseErrorJsListeners.delete(listener);
|
|
529
|
+
throw error;
|
|
503
530
|
}
|
|
504
531
|
|
|
505
532
|
return {
|
|
@@ -519,7 +546,9 @@ export const purchaseErrorListener = (
|
|
|
519
546
|
|
|
520
547
|
if (purchaseErrorNativeAttached) {
|
|
521
548
|
try {
|
|
522
|
-
|
|
549
|
+
getRawIapInstance().removePurchaseErrorListener(
|
|
550
|
+
purchaseErrorNativeHandler,
|
|
551
|
+
);
|
|
523
552
|
purchaseErrorNativeAttached = false;
|
|
524
553
|
} catch (e) {
|
|
525
554
|
RnIapConsole.warn('[purchaseErrorListener] native remove failed:', e);
|
|
@@ -548,21 +577,11 @@ export const promotedProductListenerIOS = (
|
|
|
548
577
|
}
|
|
549
578
|
|
|
550
579
|
promotedProductJsListeners.add(listener);
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
} catch (e) {
|
|
557
|
-
const msg = toErrorMessage(e);
|
|
558
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
559
|
-
RnIapConsole.warn(
|
|
560
|
-
'[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()',
|
|
561
|
-
);
|
|
562
|
-
} else {
|
|
563
|
-
throw e;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
580
|
+
try {
|
|
581
|
+
tryAttachPromotedProductNative();
|
|
582
|
+
} catch (error) {
|
|
583
|
+
promotedProductJsListeners.delete(listener);
|
|
584
|
+
throw error;
|
|
566
585
|
}
|
|
567
586
|
|
|
568
587
|
return {
|
|
@@ -588,7 +607,9 @@ export const promotedProductListenerIOS = (
|
|
|
588
607
|
* console.log('External transaction token received; send it to your backend without logging it.');
|
|
589
608
|
*
|
|
590
609
|
* // Send token to backend for Google Play reporting
|
|
591
|
-
*
|
|
610
|
+
* void reportToGooglePlay(details.externalTransactionToken).catch((error) => {
|
|
611
|
+
* console.warn('Alternative billing report failed', error);
|
|
612
|
+
* });
|
|
592
613
|
* });
|
|
593
614
|
*
|
|
594
615
|
* // Later, remove the listener
|
|
@@ -618,6 +639,16 @@ const userChoiceBillingNativeHandler: NitroUserChoiceBillingListener = (
|
|
|
618
639
|
}
|
|
619
640
|
};
|
|
620
641
|
|
|
642
|
+
function tryAttachUserChoiceBillingNative(): void {
|
|
643
|
+
if (userChoiceBillingNativeAttached) return;
|
|
644
|
+
attachNativeListenerOrDefer('userChoiceBillingListenerAndroid', () => {
|
|
645
|
+
getRawIapInstance().addUserChoiceBillingListenerAndroid(
|
|
646
|
+
userChoiceBillingNativeHandler,
|
|
647
|
+
);
|
|
648
|
+
userChoiceBillingNativeAttached = true;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
621
652
|
export const userChoiceBillingListenerAndroid = (
|
|
622
653
|
listener: (details: UserChoiceBillingDetails) => void,
|
|
623
654
|
): EventSubscription => {
|
|
@@ -629,23 +660,11 @@ export const userChoiceBillingListenerAndroid = (
|
|
|
629
660
|
}
|
|
630
661
|
|
|
631
662
|
userChoiceBillingJsListeners.add(listener);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
);
|
|
638
|
-
userChoiceBillingNativeAttached = true;
|
|
639
|
-
} catch (e) {
|
|
640
|
-
const msg = toErrorMessage(e);
|
|
641
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
642
|
-
RnIapConsole.warn(
|
|
643
|
-
'[userChoiceBillingListenerAndroid] Nitro not ready yet; listener inert until initConnection()',
|
|
644
|
-
);
|
|
645
|
-
} else {
|
|
646
|
-
throw e;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
663
|
+
try {
|
|
664
|
+
tryAttachUserChoiceBillingNative();
|
|
665
|
+
} catch (error) {
|
|
666
|
+
userChoiceBillingJsListeners.delete(listener);
|
|
667
|
+
throw error;
|
|
649
668
|
}
|
|
650
669
|
|
|
651
670
|
let removed = false;
|
|
@@ -661,7 +680,7 @@ export const userChoiceBillingListenerAndroid = (
|
|
|
661
680
|
return;
|
|
662
681
|
}
|
|
663
682
|
try {
|
|
664
|
-
|
|
683
|
+
getRawIapInstance().removeUserChoiceBillingListenerAndroid(
|
|
665
684
|
userChoiceBillingNativeHandler,
|
|
666
685
|
);
|
|
667
686
|
userChoiceBillingNativeAttached = false;
|
|
@@ -690,10 +709,11 @@ export const userChoiceBillingListenerAndroid = (
|
|
|
690
709
|
* @example
|
|
691
710
|
* ```typescript
|
|
692
711
|
* const subscription = developerProvidedBillingListenerAndroid((details) => {
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
712
|
+
* void processExternalPayment(details.products, details.linkUri)
|
|
713
|
+
* .then(() => details.externalTransactionToken
|
|
714
|
+
* ? reportToGooglePlay(details.externalTransactionToken)
|
|
715
|
+
* : undefined)
|
|
716
|
+
* .catch((error) => console.warn('Developer billing failed', error));
|
|
697
717
|
* });
|
|
698
718
|
*
|
|
699
719
|
* // Later, remove the listener
|
|
@@ -722,6 +742,16 @@ const developerProvidedBillingNativeHandler: NitroDeveloperProvidedBillingListen
|
|
|
722
742
|
}
|
|
723
743
|
};
|
|
724
744
|
|
|
745
|
+
function tryAttachDeveloperProvidedBillingNative(): void {
|
|
746
|
+
if (developerProvidedBillingNativeAttached) return;
|
|
747
|
+
attachNativeListenerOrDefer('developerProvidedBillingListenerAndroid', () => {
|
|
748
|
+
getRawIapInstance().addDeveloperProvidedBillingListenerAndroid(
|
|
749
|
+
developerProvidedBillingNativeHandler,
|
|
750
|
+
);
|
|
751
|
+
developerProvidedBillingNativeAttached = true;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
725
755
|
export const developerProvidedBillingListenerAndroid = (
|
|
726
756
|
listener: (details: DeveloperProvidedBillingDetailsAndroid) => void,
|
|
727
757
|
): EventSubscription => {
|
|
@@ -733,23 +763,11 @@ export const developerProvidedBillingListenerAndroid = (
|
|
|
733
763
|
}
|
|
734
764
|
|
|
735
765
|
developerProvidedBillingJsListeners.add(listener);
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
);
|
|
742
|
-
developerProvidedBillingNativeAttached = true;
|
|
743
|
-
} catch (e) {
|
|
744
|
-
const msg = toErrorMessage(e);
|
|
745
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
746
|
-
RnIapConsole.warn(
|
|
747
|
-
'[developerProvidedBillingListenerAndroid] Nitro not ready yet; listener inert until initConnection()',
|
|
748
|
-
);
|
|
749
|
-
} else {
|
|
750
|
-
throw e;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
766
|
+
try {
|
|
767
|
+
tryAttachDeveloperProvidedBillingNative();
|
|
768
|
+
} catch (error) {
|
|
769
|
+
developerProvidedBillingJsListeners.delete(listener);
|
|
770
|
+
throw error;
|
|
753
771
|
}
|
|
754
772
|
|
|
755
773
|
return {
|
|
@@ -814,21 +832,12 @@ const subscriptionBillingIssueNativeHandler: NitroSubscriptionBillingIssueListen
|
|
|
814
832
|
|
|
815
833
|
function tryAttachSubscriptionBillingIssueNative(): void {
|
|
816
834
|
if (subscriptionBillingIssueNativeAttached) return;
|
|
817
|
-
|
|
818
|
-
|
|
835
|
+
attachNativeListenerOrDefer('subscriptionBillingIssueListener', () => {
|
|
836
|
+
getRawIapInstance().addSubscriptionBillingIssueListener(
|
|
819
837
|
subscriptionBillingIssueNativeHandler,
|
|
820
838
|
);
|
|
821
839
|
subscriptionBillingIssueNativeAttached = true;
|
|
822
|
-
}
|
|
823
|
-
const msg = toErrorMessage(e);
|
|
824
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
825
|
-
RnIapConsole.warn(
|
|
826
|
-
'[subscriptionBillingIssueListener] Nitro not ready yet; will retry after initConnection()',
|
|
827
|
-
);
|
|
828
|
-
} else {
|
|
829
|
-
throw e;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
840
|
+
});
|
|
832
841
|
}
|
|
833
842
|
|
|
834
843
|
export const subscriptionBillingIssueListener = (
|
|
@@ -851,6 +860,30 @@ export const subscriptionBillingIssueListener = (
|
|
|
851
860
|
};
|
|
852
861
|
};
|
|
853
862
|
|
|
863
|
+
function tryAttachPendingNativeListeners(): void {
|
|
864
|
+
if (purchaseUpdateJsListeners.size > 0) {
|
|
865
|
+
tryAttachPurchaseUpdateNative(false);
|
|
866
|
+
}
|
|
867
|
+
if (purchaseUpdateDuplicateJsListeners.size > 0) {
|
|
868
|
+
tryAttachPurchaseUpdateNative(true);
|
|
869
|
+
}
|
|
870
|
+
if (purchaseErrorJsListeners.size > 0) {
|
|
871
|
+
tryAttachPurchaseErrorNative();
|
|
872
|
+
}
|
|
873
|
+
if (promotedProductJsListeners.size > 0) {
|
|
874
|
+
tryAttachPromotedProductNative();
|
|
875
|
+
}
|
|
876
|
+
if (userChoiceBillingJsListeners.size > 0) {
|
|
877
|
+
tryAttachUserChoiceBillingNative();
|
|
878
|
+
}
|
|
879
|
+
if (developerProvidedBillingJsListeners.size > 0) {
|
|
880
|
+
tryAttachDeveloperProvidedBillingNative();
|
|
881
|
+
}
|
|
882
|
+
if (subscriptionBillingIssueJsListeners.size > 0) {
|
|
883
|
+
tryAttachSubscriptionBillingIssueNative();
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
854
887
|
// ------------------------------
|
|
855
888
|
// Query API
|
|
856
889
|
// ------------------------------
|
|
@@ -1170,8 +1203,16 @@ export const getAppTransactionIOS: QueryField<
|
|
|
1170
1203
|
|
|
1171
1204
|
return null;
|
|
1172
1205
|
} catch (error) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1206
|
+
const parsedError = parseErrorAndLogIfNeeded(
|
|
1207
|
+
'Failed to get app transaction:',
|
|
1208
|
+
error,
|
|
1209
|
+
);
|
|
1210
|
+
throw createPurchaseError({
|
|
1211
|
+
code: parsedError.code,
|
|
1212
|
+
message: parsedError.message,
|
|
1213
|
+
responseCode: parsedError.responseCode,
|
|
1214
|
+
debugMessage: parsedError.debugMessage,
|
|
1215
|
+
});
|
|
1175
1216
|
}
|
|
1176
1217
|
};
|
|
1177
1218
|
|
|
@@ -1545,16 +1586,22 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
|
|
|
1545
1586
|
*
|
|
1546
1587
|
* @example
|
|
1547
1588
|
* ```ts
|
|
1548
|
-
*
|
|
1549
|
-
* await initConnection(
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1553
|
-
* });
|
|
1589
|
+
* // Choose exactly one connection call for the session.
|
|
1590
|
+
* const connected = await initConnection();
|
|
1591
|
+
* // Or replace the call above with one Android billing program:
|
|
1592
|
+
* // const connected = await initConnection({
|
|
1593
|
+
* // enableBillingProgramAndroid: 'external-offer',
|
|
1594
|
+
* // });
|
|
1595
|
+
* // const connected = await initConnection({
|
|
1596
|
+
* // enableBillingProgramAndroid: 'billing-choice',
|
|
1597
|
+
* // billingChoiceScreenTypeAndroid: 'developer-rendered',
|
|
1598
|
+
* // });
|
|
1599
|
+
* if (!connected) throw new Error('Store connection failed');
|
|
1554
1600
|
* ```
|
|
1555
1601
|
*
|
|
1556
|
-
* @remarks When using `useIAP()`, connection
|
|
1557
|
-
*
|
|
1602
|
+
* @remarks When using `useIAP()`, the connection initializes on mount. On unmount,
|
|
1603
|
+
* React Native removes hook listeners but keeps the native connection open across
|
|
1604
|
+
* screens. Pass options to the hook instead of calling this directly.
|
|
1558
1605
|
*
|
|
1559
1606
|
* @see {@link https://openiap.dev/docs/apis/init-connection}
|
|
1560
1607
|
*/
|
|
@@ -1565,9 +1612,7 @@ export const initConnection: MutationField<'initConnection'> = async (
|
|
|
1565
1612
|
const result = await IAP.instance.initConnection(
|
|
1566
1613
|
config as Record<string, unknown> | undefined,
|
|
1567
1614
|
);
|
|
1568
|
-
|
|
1569
|
-
tryAttachSubscriptionBillingIssueNative();
|
|
1570
|
-
}
|
|
1615
|
+
tryAttachPendingNativeListeners();
|
|
1571
1616
|
return result;
|
|
1572
1617
|
} catch (error) {
|
|
1573
1618
|
const parsedError = parseErrorAndLogIfNeeded(
|
|
@@ -1590,8 +1635,9 @@ export const initConnection: MutationField<'initConnection'> = async (
|
|
|
1590
1635
|
*/
|
|
1591
1636
|
export const endConnection: MutationField<'endConnection'> = async () => {
|
|
1592
1637
|
try {
|
|
1593
|
-
|
|
1594
|
-
|
|
1638
|
+
const result = iapRef ? await IAP.instance.endConnection() : true;
|
|
1639
|
+
resetListenerState();
|
|
1640
|
+
return result;
|
|
1595
1641
|
} catch (error) {
|
|
1596
1642
|
const parsedError = parseErrorAndLogIfNeeded(
|
|
1597
1643
|
'Failed to end IAP connection:',
|
|
@@ -1603,8 +1649,6 @@ export const endConnection: MutationField<'endConnection'> = async () => {
|
|
|
1603
1649
|
responseCode: parsedError.responseCode,
|
|
1604
1650
|
debugMessage: parsedError.debugMessage,
|
|
1605
1651
|
});
|
|
1606
|
-
} finally {
|
|
1607
|
-
resetListenerState();
|
|
1608
1652
|
}
|
|
1609
1653
|
};
|
|
1610
1654
|
|
|
@@ -1652,10 +1696,14 @@ export const restorePurchases: MutationField<'restorePurchases'> = async () => {
|
|
|
1652
1696
|
* - `type: 'in-app'` — pass `request.apple.sku` (iOS) and/or `request.google.skus` (Android).
|
|
1653
1697
|
* - `type: 'subs'` — same shape, plus `request.google.subscriptionOffers: [{ sku, offerToken }]`.
|
|
1654
1698
|
* @returns The dispatched purchase payload. **Do not rely on it** for the actual outcome.
|
|
1655
|
-
* @throws
|
|
1699
|
+
* @throws Invalid arguments or failures that prevent dispatch. Store outcomes are event-based;
|
|
1700
|
+
* on Android, `not-prepared` is delivered through `purchaseErrorListener`.
|
|
1656
1701
|
*
|
|
1657
1702
|
* @example
|
|
1658
1703
|
* ```ts
|
|
1704
|
+
* const connected = await initConnection();
|
|
1705
|
+
* if (!connected) return;
|
|
1706
|
+
*
|
|
1659
1707
|
* await requestPurchase({
|
|
1660
1708
|
* request: {
|
|
1661
1709
|
* apple: { sku: 'com.app.premium' },
|
|
@@ -1722,7 +1770,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
1722
1770
|
throw unsupportedPlatformError();
|
|
1723
1771
|
}
|
|
1724
1772
|
|
|
1725
|
-
const unifiedRequest: NitroPurchaseRequest = {};
|
|
1773
|
+
const unifiedRequest: NitroPurchaseRequest = {type: normalizedType};
|
|
1726
1774
|
|
|
1727
1775
|
if (Platform.OS === 'ios' && iosRequestSource) {
|
|
1728
1776
|
const iosRequest = isSubs
|
|
@@ -1901,10 +1949,12 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
1901
1949
|
*
|
|
1902
1950
|
* @example
|
|
1903
1951
|
* ```ts
|
|
1904
|
-
* purchaseUpdatedListener(
|
|
1905
|
-
*
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
1952
|
+
* purchaseUpdatedListener((purchase) => {
|
|
1953
|
+
* void verifyOnServer(purchase)
|
|
1954
|
+
* .then((verified) => verified
|
|
1955
|
+
* ? finishTransaction({ purchase, isConsumable: false })
|
|
1956
|
+
* : undefined)
|
|
1957
|
+
* .catch((error) => console.warn('Transaction finalization failed:', error));
|
|
1908
1958
|
* });
|
|
1909
1959
|
* ```
|
|
1910
1960
|
*
|
|
@@ -1952,11 +2002,11 @@ export const finishTransaction: MutationField<'finishTransaction'> = async (
|
|
|
1952
2002
|
}
|
|
1953
2003
|
return;
|
|
1954
2004
|
} catch (error) {
|
|
2005
|
+
const parsedError = parseErrorStringToJsonObj(error);
|
|
1955
2006
|
// If iOS transaction has already been auto-finished natively, treat as success
|
|
1956
2007
|
if (Platform.OS === 'ios') {
|
|
1957
|
-
const
|
|
1958
|
-
const
|
|
1959
|
-
const code = (err?.code || '').toString();
|
|
2008
|
+
const msg = (parsedError.message || '').toString();
|
|
2009
|
+
const code = (parsedError.code || '').toString();
|
|
1960
2010
|
if (
|
|
1961
2011
|
msg.includes('Transaction not found') ||
|
|
1962
2012
|
code === 'E_ITEM_UNAVAILABLE'
|
|
@@ -1965,8 +2015,15 @@ export const finishTransaction: MutationField<'finishTransaction'> = async (
|
|
|
1965
2015
|
return;
|
|
1966
2016
|
}
|
|
1967
2017
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
2018
|
+
if (!isUserCancelledError(parsedError)) {
|
|
2019
|
+
RnIapConsole.error('Failed to finish transaction:', error);
|
|
2020
|
+
}
|
|
2021
|
+
throw createPurchaseError({
|
|
2022
|
+
code: parsedError.code,
|
|
2023
|
+
message: parsedError.message,
|
|
2024
|
+
responseCode: parsedError.responseCode,
|
|
2025
|
+
debugMessage: parsedError.debugMessage,
|
|
2026
|
+
});
|
|
1970
2027
|
}
|
|
1971
2028
|
};
|
|
1972
2029
|
|
|
@@ -2638,20 +2695,15 @@ export const getActiveSubscriptions: QueryField<
|
|
|
2638
2695
|
*
|
|
2639
2696
|
* @param subscriptionIds - Optional array of subscription IDs to check
|
|
2640
2697
|
* @returns Promise<boolean> - True if there are active subscriptions
|
|
2698
|
+
* @throws When the store cannot determine subscription status
|
|
2641
2699
|
*
|
|
2642
2700
|
* @see {@link https://openiap.dev/docs/apis/has-active-subscriptions}
|
|
2643
2701
|
*/
|
|
2644
2702
|
export const hasActiveSubscriptions: QueryField<
|
|
2645
2703
|
'hasActiveSubscriptions'
|
|
2646
2704
|
> = async (subscriptionIds) => {
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
return activeSubscriptions.length > 0;
|
|
2650
|
-
} catch (error) {
|
|
2651
|
-
// If there's an error getting subscriptions, return false
|
|
2652
|
-
RnIapConsole.warn('Error checking active subscriptions:', error);
|
|
2653
|
-
return false;
|
|
2654
|
-
}
|
|
2705
|
+
const activeSubscriptions = await getActiveSubscriptions(subscriptionIds);
|
|
2706
|
+
return activeSubscriptions.length > 0;
|
|
2655
2707
|
};
|
|
2656
2708
|
|
|
2657
2709
|
// Type conversion utilities
|
|
@@ -2735,7 +2787,8 @@ const normalizeProductQueryType = (
|
|
|
2735
2787
|
* ```typescript
|
|
2736
2788
|
* // Enable external offers before connecting
|
|
2737
2789
|
* enableBillingProgramAndroid('external-offer');
|
|
2738
|
-
* await initConnection();
|
|
2790
|
+
* const connected = await initConnection();
|
|
2791
|
+
* if (!connected) throw new Error('Store connection failed');
|
|
2739
2792
|
* ```
|
|
2740
2793
|
*/
|
|
2741
2794
|
export const enableBillingProgramAndroid = (
|