react-native-iap 14.7.17 → 14.7.19
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/ios/HybridRnIap.swift +9 -0
- package/lib/module/hooks/useIAP.js +99 -72
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/index.js +5 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/error.js +3 -0
- package/lib/module/utils/error.js.map +1 -1
- package/lib/module/utils/errorMapping.js +13 -1
- package/lib/module/utils/errorMapping.js.map +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +9 -3
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/error.d.ts +1 -0
- package/lib/typescript/src/utils/error.d.ts.map +1 -1
- package/lib/typescript/src/utils/errorMapping.d.ts +7 -0
- package/lib/typescript/src/utils/errorMapping.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/hooks/useIAP.ts +129 -82
- package/src/index.ts +5 -2
- package/src/utils/error.ts +6 -0
- package/src/utils/errorMapping.ts +14 -0
package/src/hooks/useIAP.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
verifyPurchaseWithProvider as verifyPurchaseWithProviderTopLevel,
|
|
19
19
|
getActiveSubscriptions,
|
|
20
20
|
hasActiveSubscriptions,
|
|
21
|
-
|
|
21
|
+
syncIOS,
|
|
22
22
|
getPromotedProductIOS,
|
|
23
23
|
requestPurchaseOnPromotedProductIOS,
|
|
24
24
|
checkAlternativeBillingAvailabilityAndroid,
|
|
@@ -40,6 +40,7 @@ import type {
|
|
|
40
40
|
VerifyPurchaseResult,
|
|
41
41
|
VerifyPurchaseWithProviderProps,
|
|
42
42
|
VerifyPurchaseWithProviderResult,
|
|
43
|
+
PurchaseOptions,
|
|
43
44
|
} from '../types';
|
|
44
45
|
import type {
|
|
45
46
|
ActiveSubscription,
|
|
@@ -63,7 +64,7 @@ type UseIap = {
|
|
|
63
64
|
promotedProductIOS?: Product;
|
|
64
65
|
activeSubscriptions: ActiveSubscription[];
|
|
65
66
|
finishTransaction: (args: MutationFinishTransactionArgs) => Promise<void>;
|
|
66
|
-
getAvailablePurchases: (
|
|
67
|
+
getAvailablePurchases: (options?: PurchaseOptions) => Promise<void>;
|
|
67
68
|
fetchProducts: (params: {
|
|
68
69
|
skus: string[];
|
|
69
70
|
type?: ProductQueryType | null;
|
|
@@ -83,13 +84,19 @@ type UseIap = {
|
|
|
83
84
|
verifyPurchaseWithProvider: (
|
|
84
85
|
options: VerifyPurchaseWithProviderProps,
|
|
85
86
|
) => Promise<VerifyPurchaseWithProviderResult>;
|
|
86
|
-
restorePurchases: () => Promise<void>;
|
|
87
|
+
restorePurchases: (options?: PurchaseOptions) => Promise<void>;
|
|
87
88
|
getPromotedProductIOS: () => Promise<Product | null>;
|
|
88
89
|
requestPurchaseOnPromotedProductIOS: () => Promise<boolean>;
|
|
89
90
|
getActiveSubscriptions: (
|
|
90
91
|
subscriptionIds?: string[],
|
|
91
92
|
) => Promise<ActiveSubscription[]>;
|
|
92
93
|
hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
|
|
94
|
+
/**
|
|
95
|
+
* Manually retry the store connection.
|
|
96
|
+
* Useful when the initial auto-connect fails (e.g., Play Store not ready at mount time).
|
|
97
|
+
* Updates the `connected` state on success.
|
|
98
|
+
*/
|
|
99
|
+
reconnect: () => Promise<boolean>;
|
|
93
100
|
// Alternative billing (Android)
|
|
94
101
|
checkAlternativeBillingAvailabilityAndroid?: () => Promise<boolean>;
|
|
95
102
|
showAlternativeBillingDialogAndroid?: () => Promise<boolean>;
|
|
@@ -271,11 +278,13 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
271
278
|
);
|
|
272
279
|
|
|
273
280
|
const getAvailablePurchasesInternal = useCallback(
|
|
274
|
-
async (
|
|
281
|
+
async (options?: PurchaseOptions): Promise<void> => {
|
|
275
282
|
try {
|
|
276
283
|
const result = await getAvailablePurchases({
|
|
277
|
-
alsoPublishToEventListenerIOS:
|
|
278
|
-
|
|
284
|
+
alsoPublishToEventListenerIOS:
|
|
285
|
+
options?.alsoPublishToEventListenerIOS ?? false,
|
|
286
|
+
onlyIncludeActiveItemsIOS: options?.onlyIncludeActiveItemsIOS ?? true,
|
|
287
|
+
includeSuspendedAndroid: options?.includeSuspendedAndroid ?? false,
|
|
279
288
|
});
|
|
280
289
|
setAvailablePurchases(result);
|
|
281
290
|
} catch (error) {
|
|
@@ -333,7 +342,21 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
333
342
|
[],
|
|
334
343
|
);
|
|
335
344
|
|
|
336
|
-
|
|
345
|
+
const restorePurchases = useCallback(
|
|
346
|
+
async (options?: PurchaseOptions): Promise<void> => {
|
|
347
|
+
try {
|
|
348
|
+
if (Platform.OS === 'ios') {
|
|
349
|
+
await syncIOS();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await getAvailablePurchasesInternal(options);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
RnIapConsole.warn('Failed to restore purchases:', error);
|
|
355
|
+
invokeOnError(error);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
[getAvailablePurchasesInternal, invokeOnError],
|
|
359
|
+
);
|
|
337
360
|
|
|
338
361
|
const validateReceipt = useCallback(
|
|
339
362
|
async (options: VerifyPurchaseProps): Promise<VerifyPurchaseResult> =>
|
|
@@ -357,9 +380,8 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
357
380
|
[],
|
|
358
381
|
);
|
|
359
382
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// Prefer enableBillingProgramAndroid over deprecated alternativeBillingModeAndroid
|
|
383
|
+
// Shared helper: build Android billing config from options
|
|
384
|
+
const buildAndroidConfig = useCallback(() => {
|
|
363
385
|
let config:
|
|
364
386
|
| {
|
|
365
387
|
enableBillingProgramAndroid?: BillingProgramAndroid;
|
|
@@ -374,7 +396,6 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
374
396
|
optionsRef.current.enableBillingProgramAndroid,
|
|
375
397
|
};
|
|
376
398
|
} else if (optionsRef.current?.alternativeBillingModeAndroid) {
|
|
377
|
-
// Deprecated: use alternativeBillingModeAndroid for backwards compatibility
|
|
378
399
|
config = {
|
|
379
400
|
alternativeBillingModeAndroid:
|
|
380
401
|
optionsRef.current.alternativeBillingModeAndroid,
|
|
@@ -382,29 +403,14 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
382
403
|
}
|
|
383
404
|
}
|
|
384
405
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
// This fixes tvOS where Nitro may initialize later than iOS
|
|
388
|
-
const result = await initConnection(config);
|
|
389
|
-
|
|
390
|
-
// Check if component unmounted during async initConnection
|
|
391
|
-
// to prevent listener leaks
|
|
392
|
-
if (!isMountedRef.current) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
setConnected(result);
|
|
397
|
-
|
|
398
|
-
if (!result) {
|
|
399
|
-
RnIapConsole.warn('[useIAP] initConnection returned false');
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
406
|
+
return config;
|
|
407
|
+
}, []);
|
|
402
408
|
|
|
403
|
-
|
|
404
|
-
|
|
409
|
+
// Shared helper: register event listeners if not already active
|
|
410
|
+
const registerListeners = useCallback(() => {
|
|
411
|
+
if (!subscriptionsRef.current.purchaseUpdate) {
|
|
405
412
|
subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
|
|
406
413
|
async (purchase: Purchase) => {
|
|
407
|
-
// Always refresh subscription state after a purchase event
|
|
408
414
|
try {
|
|
409
415
|
await getActiveSubscriptionsInternal();
|
|
410
416
|
await getAvailablePurchasesInternal();
|
|
@@ -416,11 +422,11 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
416
422
|
}
|
|
417
423
|
},
|
|
418
424
|
);
|
|
425
|
+
}
|
|
419
426
|
|
|
427
|
+
if (!subscriptionsRef.current.purchaseError) {
|
|
420
428
|
subscriptionsRef.current.purchaseError = purchaseErrorListener(
|
|
421
429
|
(error) => {
|
|
422
|
-
// error is already normalized by purchaseErrorListener in src/index.ts
|
|
423
|
-
// Ignore init error until connected
|
|
424
430
|
if (
|
|
425
431
|
error.code === ErrorCode.InitConnection &&
|
|
426
432
|
!connectedRef.current
|
|
@@ -432,65 +438,113 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
432
438
|
}
|
|
433
439
|
},
|
|
434
440
|
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (isStandardIOS() && !subscriptionsRef.current.promotedProductIOS) {
|
|
444
|
+
subscriptionsRef.current.promotedProductIOS = promotedProductListenerIOS(
|
|
445
|
+
(product: Product) => {
|
|
446
|
+
setPromotedProductIOS(product);
|
|
447
|
+
if (optionsRef.current?.onPromotedProductIOS) {
|
|
448
|
+
optionsRef.current.onPromotedProductIOS(product);
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (
|
|
455
|
+
Platform.OS === 'android' &&
|
|
456
|
+
optionsRef.current?.onUserChoiceBillingAndroid &&
|
|
457
|
+
!subscriptionsRef.current.userChoiceBillingAndroid
|
|
458
|
+
) {
|
|
459
|
+
subscriptionsRef.current.userChoiceBillingAndroid =
|
|
460
|
+
userChoiceBillingListenerAndroid((details) => {
|
|
461
|
+
if (optionsRef.current?.onUserChoiceBillingAndroid) {
|
|
462
|
+
optionsRef.current.onUserChoiceBillingAndroid(details);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}, [getActiveSubscriptionsInternal, getAvailablePurchasesInternal]);
|
|
467
|
+
|
|
468
|
+
// Shared helper: clean up all listeners
|
|
469
|
+
const cleanupListeners = useCallback(() => {
|
|
470
|
+
subscriptionsRef.current.purchaseUpdate?.remove();
|
|
471
|
+
subscriptionsRef.current.purchaseError?.remove();
|
|
472
|
+
subscriptionsRef.current.promotedProductIOS?.remove();
|
|
473
|
+
subscriptionsRef.current.userChoiceBillingAndroid?.remove();
|
|
474
|
+
subscriptionsRef.current.purchaseUpdate = undefined;
|
|
475
|
+
subscriptionsRef.current.purchaseError = undefined;
|
|
476
|
+
subscriptionsRef.current.promotedProductIOS = undefined;
|
|
477
|
+
subscriptionsRef.current.userChoiceBillingAndroid = undefined;
|
|
478
|
+
}, []);
|
|
435
479
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
subscriptionsRef.current.promotedProductIOS =
|
|
439
|
-
promotedProductListenerIOS((product: Product) => {
|
|
440
|
-
setPromotedProductIOS(product);
|
|
480
|
+
const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
|
|
481
|
+
const config = buildAndroidConfig();
|
|
441
482
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
483
|
+
try {
|
|
484
|
+
const result = await initConnection(config);
|
|
485
|
+
|
|
486
|
+
if (!isMountedRef.current) {
|
|
487
|
+
return;
|
|
446
488
|
}
|
|
447
489
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
) {
|
|
453
|
-
subscriptionsRef.current.userChoiceBillingAndroid =
|
|
454
|
-
userChoiceBillingListenerAndroid((details) => {
|
|
455
|
-
if (optionsRef.current?.onUserChoiceBillingAndroid) {
|
|
456
|
-
optionsRef.current.onUserChoiceBillingAndroid(details);
|
|
457
|
-
}
|
|
458
|
-
});
|
|
490
|
+
if (!result) {
|
|
491
|
+
setConnected(false);
|
|
492
|
+
RnIapConsole.warn('[useIAP] initConnection returned false');
|
|
493
|
+
return;
|
|
459
494
|
}
|
|
495
|
+
|
|
496
|
+
registerListeners();
|
|
497
|
+
setConnected(true);
|
|
460
498
|
} catch (error) {
|
|
461
499
|
RnIapConsole.error('initConnection failed:', error);
|
|
500
|
+
cleanupListeners();
|
|
501
|
+
if (isMountedRef.current) {
|
|
502
|
+
setConnected(false);
|
|
503
|
+
}
|
|
462
504
|
invokeOnError(error);
|
|
463
|
-
// Clean up listeners on error (if any were registered)
|
|
464
|
-
subscriptionsRef.current.purchaseUpdate?.remove();
|
|
465
|
-
subscriptionsRef.current.purchaseError?.remove();
|
|
466
|
-
subscriptionsRef.current.promotedProductIOS?.remove();
|
|
467
|
-
subscriptionsRef.current.userChoiceBillingAndroid?.remove();
|
|
468
|
-
subscriptionsRef.current.purchaseUpdate = undefined;
|
|
469
|
-
subscriptionsRef.current.purchaseError = undefined;
|
|
470
|
-
subscriptionsRef.current.promotedProductIOS = undefined;
|
|
471
|
-
subscriptionsRef.current.userChoiceBillingAndroid = undefined;
|
|
472
505
|
}
|
|
473
|
-
}, [
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
506
|
+
}, [buildAndroidConfig, registerListeners, cleanupListeners, invokeOnError]);
|
|
507
|
+
|
|
508
|
+
const reconnect = useCallback(async (): Promise<boolean> => {
|
|
509
|
+
const config = buildAndroidConfig();
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const result = await initConnection(config);
|
|
513
|
+
|
|
514
|
+
if (!isMountedRef.current) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (result) {
|
|
519
|
+
registerListeners();
|
|
520
|
+
setConnected(true);
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
setConnected(false);
|
|
525
|
+
return false;
|
|
526
|
+
} catch (error) {
|
|
527
|
+
RnIapConsole.error('[useIAP] reconnect failed:', error);
|
|
528
|
+
cleanupListeners();
|
|
529
|
+
if (isMountedRef.current) {
|
|
530
|
+
setConnected(false);
|
|
531
|
+
}
|
|
532
|
+
invokeOnError(error);
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}, [buildAndroidConfig, registerListeners, cleanupListeners, invokeOnError]);
|
|
478
536
|
|
|
479
537
|
useEffect(() => {
|
|
480
538
|
isMountedRef.current = true;
|
|
481
539
|
initIapWithSubscriptions();
|
|
482
|
-
const currentSubscriptions = subscriptionsRef.current;
|
|
483
540
|
|
|
484
541
|
return () => {
|
|
485
542
|
isMountedRef.current = false;
|
|
486
|
-
|
|
487
|
-
currentSubscriptions.purchaseError?.remove();
|
|
488
|
-
currentSubscriptions.promotedProductIOS?.remove();
|
|
489
|
-
currentSubscriptions.userChoiceBillingAndroid?.remove();
|
|
543
|
+
cleanupListeners();
|
|
490
544
|
// Keep connection alive across screens to avoid race conditions
|
|
491
545
|
setConnected(false);
|
|
492
546
|
};
|
|
493
|
-
}, [initIapWithSubscriptions]);
|
|
547
|
+
}, [initIapWithSubscriptions, cleanupListeners]);
|
|
494
548
|
|
|
495
549
|
return {
|
|
496
550
|
connected,
|
|
@@ -506,19 +560,12 @@ export function useIAP(options?: UseIapOptions): UseIap {
|
|
|
506
560
|
validateReceipt,
|
|
507
561
|
verifyPurchase,
|
|
508
562
|
verifyPurchaseWithProvider,
|
|
509
|
-
restorePurchases
|
|
510
|
-
try {
|
|
511
|
-
await restorePurchasesTopLevel();
|
|
512
|
-
await getAvailablePurchasesInternal();
|
|
513
|
-
} catch (error) {
|
|
514
|
-
RnIapConsole.warn('Failed to restore purchases:', error);
|
|
515
|
-
invokeOnError(error);
|
|
516
|
-
}
|
|
517
|
-
},
|
|
563
|
+
restorePurchases,
|
|
518
564
|
getPromotedProductIOS,
|
|
519
565
|
requestPurchaseOnPromotedProductIOS,
|
|
520
566
|
getActiveSubscriptions: getActiveSubscriptionsInternal,
|
|
521
567
|
hasActiveSubscriptions: hasActiveSubscriptionsInternal,
|
|
568
|
+
reconnect,
|
|
522
569
|
// Alternative billing (Android only)
|
|
523
570
|
...(Platform.OS === 'android'
|
|
524
571
|
? {
|
package/src/index.ts
CHANGED
|
@@ -737,11 +737,14 @@ export const getAvailablePurchases: QueryField<
|
|
|
737
737
|
return validPurchases.map(convertNitroPurchaseToPurchase);
|
|
738
738
|
} else if (Platform.OS === 'android') {
|
|
739
739
|
// For Android, we need to call twice for inapp and subs
|
|
740
|
+
const includeSuspended = Boolean(
|
|
741
|
+
options?.includeSuspendedAndroid ?? false,
|
|
742
|
+
);
|
|
740
743
|
const inappNitroPurchases = await IAP.instance.getAvailablePurchases({
|
|
741
|
-
android: {type: 'inapp'},
|
|
744
|
+
android: {type: 'inapp', includeSuspended},
|
|
742
745
|
});
|
|
743
746
|
const subsNitroPurchases = await IAP.instance.getAvailablePurchases({
|
|
744
|
-
android: {type: 'subs'},
|
|
747
|
+
android: {type: 'subs', includeSuspended},
|
|
745
748
|
});
|
|
746
749
|
|
|
747
750
|
// Validate and convert both sets of purchases
|
package/src/utils/error.ts
CHANGED
|
@@ -95,3 +95,9 @@ export function isUserCancelledError(
|
|
|
95
95
|
errorObj.responseCode === 1
|
|
96
96
|
); // Android BillingClient.BillingResponseCode.USER_CANCELED
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
// Re-export from errorMapping for public API convenience
|
|
100
|
+
export {
|
|
101
|
+
isDuplicatePurchaseError,
|
|
102
|
+
DUPLICATE_PURCHASE_CODE,
|
|
103
|
+
} from './errorMapping';
|
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import {ErrorCode, type IapPlatform} from '../types';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Error code for duplicate purchase events detected on iOS.
|
|
11
|
+
* Defined here because it originates from react-native-iap's dedup logic,
|
|
12
|
+
* not from the OpenIAP upstream that generates src/types.ts.
|
|
13
|
+
*/
|
|
14
|
+
export const DUPLICATE_PURCHASE_CODE = 'duplicate-purchase' as const;
|
|
15
|
+
|
|
9
16
|
const ERROR_CODE_ALIASES: Record<string, ErrorCode> = {
|
|
10
17
|
E_USER_CANCELED: ErrorCode.UserCancelled,
|
|
11
18
|
USER_CANCELED: ErrorCode.UserCancelled,
|
|
@@ -271,6 +278,10 @@ export function isUserCancelledError(error: unknown): boolean {
|
|
|
271
278
|
return extractCode(error) === ErrorCode.UserCancelled;
|
|
272
279
|
}
|
|
273
280
|
|
|
281
|
+
export function isDuplicatePurchaseError(error: unknown): boolean {
|
|
282
|
+
return extractCode(error) === DUPLICATE_PURCHASE_CODE;
|
|
283
|
+
}
|
|
284
|
+
|
|
274
285
|
export function isNetworkError(error: unknown): boolean {
|
|
275
286
|
const networkErrors: ErrorCode[] = [
|
|
276
287
|
ErrorCode.NetworkError,
|
|
@@ -296,6 +307,7 @@ export function isRecoverableError(error: unknown): boolean {
|
|
|
296
307
|
ErrorCode.InitConnection,
|
|
297
308
|
ErrorCode.SyncError,
|
|
298
309
|
ErrorCode.ConnectionClosed,
|
|
310
|
+
DUPLICATE_PURCHASE_CODE,
|
|
299
311
|
];
|
|
300
312
|
|
|
301
313
|
const code = extractCode(error);
|
|
@@ -326,6 +338,8 @@ export function getUserFriendlyErrorMessage(error: ErrorLike): string {
|
|
|
326
338
|
return 'Selected offer does not match the SKU';
|
|
327
339
|
case ErrorCode.DeferredPayment:
|
|
328
340
|
return 'Payment is pending approval';
|
|
341
|
+
case DUPLICATE_PURCHASE_CODE:
|
|
342
|
+
return 'This purchase has already been processed. Try restoring purchases.';
|
|
329
343
|
case ErrorCode.NotPrepared:
|
|
330
344
|
return 'In-app purchase is not ready. Please try again later.';
|
|
331
345
|
case ErrorCode.ServiceError:
|