expo-iap 2.9.0 → 2.9.2
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/CHANGELOG.md +27 -1
- package/CONTRIBUTING.md +2 -2
- package/README.md +3 -3
- package/build/ExpoIap.types.d.ts +33 -15
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js +64 -17
- package/build/ExpoIap.types.js.map +1 -1
- package/build/index.d.ts +15 -11
- package/build/index.d.ts.map +1 -1
- package/build/index.js +48 -23
- package/build/index.js.map +1 -1
- package/build/modules/ios.d.ts +3 -7
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +1 -6
- package/build/modules/ios.js.map +1 -1
- package/build/types/ExpoIapAndroid.types.d.ts +0 -32
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
- package/build/types/ExpoIapAndroid.types.js +1 -5
- package/build/types/ExpoIapAndroid.types.js.map +1 -1
- package/build/types/ExpoIapIOS.types.d.ts +3 -27
- package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
- package/build/types/ExpoIapIOS.types.js.map +1 -1
- package/build/useIAP.d.ts +1 -5
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +47 -41
- package/build/useIAP.js.map +1 -1
- package/build/utils/errorMapping.d.ts.map +1 -1
- package/build/utils/errorMapping.js +24 -0
- package/build/utils/errorMapping.js.map +1 -1
- package/ios/ExpoIap.podspec +1 -1
- package/ios/ExpoIapModule.swift +73 -52
- package/package.json +1 -1
- package/src/ExpoIap.types.ts +84 -37
- package/src/index.ts +60 -49
- package/src/modules/ios.ts +4 -9
- package/src/types/ExpoIapAndroid.types.ts +2 -36
- package/src/types/ExpoIapIOS.types.ts +3 -27
- package/src/useIAP.ts +53 -48
- package/src/utils/errorMapping.ts +24 -0
|
@@ -32,22 +32,6 @@ export type ProductIOS = ProductCommon & {
|
|
|
32
32
|
jsonRepresentationIOS: string;
|
|
33
33
|
platform: 'ios';
|
|
34
34
|
subscriptionInfoIOS?: SubscriptionInfo;
|
|
35
|
-
/**
|
|
36
|
-
* @deprecated Use `displayNameIOS` instead. This field will be removed in v2.9.0.
|
|
37
|
-
*/
|
|
38
|
-
displayName?: string;
|
|
39
|
-
/**
|
|
40
|
-
* @deprecated Use `isFamilyShareableIOS` instead. This field will be removed in v2.9.0.
|
|
41
|
-
*/
|
|
42
|
-
isFamilyShareable?: boolean;
|
|
43
|
-
/**
|
|
44
|
-
* @deprecated Use `jsonRepresentationIOS` instead. This field will be removed in v2.9.0.
|
|
45
|
-
*/
|
|
46
|
-
jsonRepresentation?: string;
|
|
47
|
-
/**
|
|
48
|
-
* @deprecated Use `subscriptionInfoIOS` instead. This field will be removed in v2.9.0.
|
|
49
|
-
*/
|
|
50
|
-
subscription?: SubscriptionInfo;
|
|
51
35
|
introductoryPriceNumberOfPeriodsIOS?: string;
|
|
52
36
|
introductoryPriceSubscriptionPeriodIOS?: SubscriptionIosPeriod;
|
|
53
37
|
};
|
|
@@ -72,14 +56,6 @@ export type ProductSubscriptionIOS = ProductIOS & {
|
|
|
72
56
|
platform: 'ios';
|
|
73
57
|
subscriptionPeriodNumberIOS?: string;
|
|
74
58
|
subscriptionPeriodUnitIOS?: SubscriptionIosPeriod;
|
|
75
|
-
/**
|
|
76
|
-
* @deprecated Use `discountsIOS` instead. This field will be removed in v2.9.0.
|
|
77
|
-
*/
|
|
78
|
-
discounts?: Discount[];
|
|
79
|
-
/**
|
|
80
|
-
* @deprecated Use `introductoryPriceIOS` instead. This field will be removed in v2.9.0.
|
|
81
|
-
*/
|
|
82
|
-
introductoryPrice?: string;
|
|
83
59
|
};
|
|
84
60
|
|
|
85
61
|
// Legacy naming for backward compatibility
|
|
@@ -119,7 +95,7 @@ export type RequestPurchaseIosProps = {
|
|
|
119
95
|
withOffer?: PaymentDiscount;
|
|
120
96
|
};
|
|
121
97
|
|
|
122
|
-
type
|
|
98
|
+
type SubscriptionState =
|
|
123
99
|
| 'expired'
|
|
124
100
|
| 'inBillingRetryPeriod'
|
|
125
101
|
| 'inGracePeriod'
|
|
@@ -132,8 +108,8 @@ type RenewalInfo = {
|
|
|
132
108
|
autoRenewPreference?: string;
|
|
133
109
|
};
|
|
134
110
|
|
|
135
|
-
export type
|
|
136
|
-
state:
|
|
111
|
+
export type SubscriptionStatusIOS = {
|
|
112
|
+
state: SubscriptionState;
|
|
137
113
|
renewalInfo?: RenewalInfo;
|
|
138
114
|
};
|
|
139
115
|
|
package/src/useIAP.ts
CHANGED
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
purchaseUpdatedListener,
|
|
12
12
|
promotedProductListenerIOS,
|
|
13
13
|
getAvailablePurchases,
|
|
14
|
-
getPurchaseHistories,
|
|
15
14
|
finishTransaction as finishTransactionInternal,
|
|
16
15
|
requestPurchase as requestPurchaseInternal,
|
|
17
16
|
fetchProducts,
|
|
@@ -19,9 +18,9 @@ import {
|
|
|
19
18
|
getActiveSubscriptions,
|
|
20
19
|
hasActiveSubscriptions,
|
|
21
20
|
type ActiveSubscription,
|
|
21
|
+
restorePurchases,
|
|
22
22
|
} from '.';
|
|
23
23
|
import {
|
|
24
|
-
syncIOS,
|
|
25
24
|
getPromotedProductIOS,
|
|
26
25
|
requestPurchaseOnPromotedProductIOS,
|
|
27
26
|
} from './modules/ios';
|
|
@@ -35,7 +34,13 @@ import {
|
|
|
35
34
|
SubscriptionProduct,
|
|
36
35
|
RequestPurchaseProps,
|
|
37
36
|
RequestSubscriptionProps,
|
|
37
|
+
ErrorCode,
|
|
38
38
|
} from './ExpoIap.types';
|
|
39
|
+
import {
|
|
40
|
+
getUserFriendlyErrorMessage,
|
|
41
|
+
isUserCancelledError,
|
|
42
|
+
isRecoverableError,
|
|
43
|
+
} from './utils/errorMapping';
|
|
39
44
|
|
|
40
45
|
type UseIap = {
|
|
41
46
|
connected: boolean;
|
|
@@ -43,7 +48,6 @@ type UseIap = {
|
|
|
43
48
|
promotedProductsIOS: Purchase[];
|
|
44
49
|
promotedProductIdIOS?: string;
|
|
45
50
|
subscriptions: SubscriptionProduct[];
|
|
46
|
-
purchaseHistories: Purchase[];
|
|
47
51
|
availablePurchases: Purchase[];
|
|
48
52
|
currentPurchase?: Purchase;
|
|
49
53
|
currentPurchaseError?: PurchaseError;
|
|
@@ -59,7 +63,6 @@ type UseIap = {
|
|
|
59
63
|
isConsumable?: boolean;
|
|
60
64
|
}) => Promise<PurchaseResult | boolean>;
|
|
61
65
|
getAvailablePurchases: (skus: string[]) => Promise<void>;
|
|
62
|
-
getPurchaseHistories: (skus: string[]) => Promise<void>;
|
|
63
66
|
fetchProducts: (params: {
|
|
64
67
|
skus: string[];
|
|
65
68
|
type?: 'inapp' | 'subs';
|
|
@@ -98,8 +101,6 @@ type UseIap = {
|
|
|
98
101
|
restorePurchases: () => Promise<void>;
|
|
99
102
|
getPromotedProductIOS: () => Promise<Product | null>;
|
|
100
103
|
requestPurchaseOnPromotedProductIOS: () => Promise<void>;
|
|
101
|
-
/** @deprecated Use requestPurchaseOnPromotedProductIOS instead */
|
|
102
|
-
buyPromotedProductIOS: () => Promise<void>;
|
|
103
104
|
getActiveSubscriptions: (subscriptionIds?: string[]) => Promise<void>;
|
|
104
105
|
hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
|
|
105
106
|
};
|
|
@@ -114,14 +115,14 @@ export interface UseIAPOptions {
|
|
|
114
115
|
|
|
115
116
|
/**
|
|
116
117
|
* React Hook for managing In-App Purchases.
|
|
117
|
-
* See documentation at https://expo-iap
|
|
118
|
+
* See documentation at https://hyochan.github.io/expo-iap/docs/hooks/useIAP
|
|
118
119
|
*/
|
|
119
120
|
export function useIAP(options?: UseIAPOptions): UseIap {
|
|
120
121
|
const [connected, setConnected] = useState<boolean>(false);
|
|
121
122
|
const [products, setProducts] = useState<Product[]>([]);
|
|
122
123
|
const [promotedProductsIOS] = useState<Purchase[]>([]);
|
|
123
124
|
const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([]);
|
|
124
|
-
|
|
125
|
+
// Removed in v2.9.0: purchaseHistories state and related API
|
|
125
126
|
const [availablePurchases, setAvailablePurchases] = useState<Purchase[]>([]);
|
|
126
127
|
const [currentPurchase, setCurrentPurchase] = useState<Purchase>();
|
|
127
128
|
const [promotedProductIOS, setPromotedProductIOS] = useState<Product>();
|
|
@@ -133,6 +134,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
133
134
|
>([]);
|
|
134
135
|
|
|
135
136
|
const optionsRef = useRef<UseIAPOptions | undefined>(options);
|
|
137
|
+
const connectedRef = useRef<boolean>(false);
|
|
136
138
|
|
|
137
139
|
// Helper function to merge arrays with duplicate checking
|
|
138
140
|
const mergeWithDuplicateCheck = useCallback(
|
|
@@ -159,6 +161,10 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
159
161
|
optionsRef.current = options;
|
|
160
162
|
}, [options]);
|
|
161
163
|
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
connectedRef.current = connected;
|
|
166
|
+
}, [connected]);
|
|
167
|
+
|
|
162
168
|
const subscriptionsRef = useRef<{
|
|
163
169
|
purchaseUpdate?: EventSubscription;
|
|
164
170
|
purchaseError?: EventSubscription;
|
|
@@ -298,13 +304,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
298
304
|
[],
|
|
299
305
|
);
|
|
300
306
|
|
|
301
|
-
|
|
302
|
-
* @deprecated Use getAvailablePurchases instead. This function is just calling getAvailablePurchases internally.
|
|
303
|
-
* Will be removed in v2.9.0
|
|
304
|
-
*/
|
|
305
|
-
const getPurchaseHistoriesInternal = useCallback(async (): Promise<void> => {
|
|
306
|
-
setPurchaseHistories(await getPurchaseHistories());
|
|
307
|
-
}, []);
|
|
307
|
+
// NOTE: getPurchaseHistories removed in v2.9.0. Use getAvailablePurchases instead.
|
|
308
308
|
|
|
309
309
|
const finishTransaction = useCallback(
|
|
310
310
|
async ({
|
|
@@ -366,22 +366,20 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
366
366
|
[getAvailablePurchasesInternal, getSubscriptionsInternal],
|
|
367
367
|
);
|
|
368
368
|
|
|
369
|
-
|
|
369
|
+
// Restore completed transactions with cross-platform behavior.
|
|
370
|
+
// iOS: best-effort sync (ignore sync errors) then fetch available purchases.
|
|
371
|
+
// Android: fetch available purchases directly.
|
|
372
|
+
const restorePurchasesInternal = useCallback(async (): Promise<void> => {
|
|
370
373
|
try {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
console.warn('Error restoring purchases:', error);
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
await getAvailablePurchasesInternal();
|
|
374
|
+
const purchases = await restorePurchases({
|
|
375
|
+
alsoPublishToEventListenerIOS: false,
|
|
376
|
+
onlyIncludeActiveItemsIOS: true,
|
|
377
|
+
});
|
|
378
|
+
setAvailablePurchases(purchases);
|
|
381
379
|
} catch (error) {
|
|
382
380
|
console.warn('Failed to restore purchases:', error);
|
|
383
381
|
}
|
|
384
|
-
}, [
|
|
382
|
+
}, []);
|
|
385
383
|
|
|
386
384
|
const validateReceipt = useCallback(
|
|
387
385
|
async (
|
|
@@ -420,9 +418,32 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
420
418
|
},
|
|
421
419
|
);
|
|
422
420
|
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
421
|
+
// Register purchase error listener EARLY. Ignore init-related errors until connected.
|
|
422
|
+
subscriptionsRef.current.purchaseError = purchaseErrorListener(
|
|
423
|
+
(error: PurchaseError) => {
|
|
424
|
+
if (
|
|
425
|
+
!connectedRef.current &&
|
|
426
|
+
error.code === ErrorCode.E_INIT_CONNECTION
|
|
427
|
+
) {
|
|
428
|
+
return; // Ignore initialization error before connected
|
|
429
|
+
}
|
|
430
|
+
const friendly = getUserFriendlyErrorMessage(error);
|
|
431
|
+
console.log('[useIAP] Purchase error callback triggered:', error);
|
|
432
|
+
if (isUserCancelledError(error)) {
|
|
433
|
+
console.log('[useIAP] User cancelled purchase');
|
|
434
|
+
} else if (isRecoverableError(error)) {
|
|
435
|
+
console.log('[useIAP] Recoverable purchase error:', friendly);
|
|
436
|
+
} else {
|
|
437
|
+
console.warn('[useIAP] Purchase error:', friendly);
|
|
438
|
+
}
|
|
439
|
+
setCurrentPurchase(undefined);
|
|
440
|
+
setCurrentPurchaseError(error);
|
|
441
|
+
|
|
442
|
+
if (optionsRef.current?.onPurchaseError) {
|
|
443
|
+
optionsRef.current.onPurchaseError(error);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
);
|
|
426
447
|
|
|
427
448
|
if (Platform.OS === 'ios') {
|
|
428
449
|
// iOS promoted products listener
|
|
@@ -453,22 +474,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
453
474
|
subscriptionsRef.current.promotedProductsIOS?.remove();
|
|
454
475
|
subscriptionsRef.current.purchaseUpdate = undefined;
|
|
455
476
|
subscriptionsRef.current.promotedProductsIOS = undefined;
|
|
456
|
-
//
|
|
477
|
+
// Keep purchaseError listener registered to capture subsequent retries
|
|
457
478
|
return;
|
|
458
479
|
}
|
|
459
|
-
|
|
460
|
-
// Now that the connection is established, register the purchase error listener.
|
|
461
|
-
subscriptionsRef.current.purchaseError = purchaseErrorListener(
|
|
462
|
-
(error: PurchaseError) => {
|
|
463
|
-
console.log('[useIAP] Purchase error callback triggered:', error);
|
|
464
|
-
setCurrentPurchase(undefined);
|
|
465
|
-
setCurrentPurchaseError(error);
|
|
466
|
-
|
|
467
|
-
if (optionsRef.current?.onPurchaseError) {
|
|
468
|
-
optionsRef.current.onPurchaseError(error);
|
|
469
|
-
}
|
|
470
|
-
},
|
|
471
|
-
);
|
|
472
480
|
}, [refreshSubscriptionStatus]);
|
|
473
481
|
|
|
474
482
|
useEffect(() => {
|
|
@@ -491,7 +499,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
491
499
|
promotedProductsIOS,
|
|
492
500
|
promotedProductIdIOS,
|
|
493
501
|
subscriptions,
|
|
494
|
-
purchaseHistories,
|
|
495
502
|
finishTransaction,
|
|
496
503
|
availablePurchases,
|
|
497
504
|
currentPurchase,
|
|
@@ -501,17 +508,15 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
501
508
|
clearCurrentPurchase,
|
|
502
509
|
clearCurrentPurchaseError,
|
|
503
510
|
getAvailablePurchases: getAvailablePurchasesInternal,
|
|
504
|
-
getPurchaseHistories: getPurchaseHistoriesInternal,
|
|
505
511
|
fetchProducts: fetchProductsInternal,
|
|
506
512
|
requestProducts: requestProductsInternal,
|
|
507
513
|
requestPurchase: requestPurchaseWithReset,
|
|
508
514
|
validateReceipt,
|
|
509
|
-
restorePurchases,
|
|
515
|
+
restorePurchases: restorePurchasesInternal,
|
|
510
516
|
getProducts: getProductsInternal,
|
|
511
517
|
getSubscriptions: getSubscriptionsInternal,
|
|
512
518
|
getPromotedProductIOS,
|
|
513
519
|
requestPurchaseOnPromotedProductIOS,
|
|
514
|
-
buyPromotedProductIOS: requestPurchaseOnPromotedProductIOS, // deprecated alias
|
|
515
520
|
getActiveSubscriptions: getActiveSubscriptionsInternal,
|
|
516
521
|
hasActiveSubscriptions: hasActiveSubscriptionsInternal,
|
|
517
522
|
};
|
|
@@ -32,6 +32,8 @@ export function isNetworkError(error: any): boolean {
|
|
|
32
32
|
ErrorCode.E_NETWORK_ERROR,
|
|
33
33
|
ErrorCode.E_REMOTE_ERROR,
|
|
34
34
|
ErrorCode.E_SERVICE_ERROR,
|
|
35
|
+
ErrorCode.E_SERVICE_DISCONNECTED,
|
|
36
|
+
ErrorCode.E_BILLING_UNAVAILABLE,
|
|
35
37
|
];
|
|
36
38
|
|
|
37
39
|
const errorCode = typeof error === 'string' ? error : error?.code;
|
|
@@ -49,6 +51,10 @@ export function isRecoverableError(error: any): boolean {
|
|
|
49
51
|
ErrorCode.E_REMOTE_ERROR,
|
|
50
52
|
ErrorCode.E_SERVICE_ERROR,
|
|
51
53
|
ErrorCode.E_INTERRUPTED,
|
|
54
|
+
ErrorCode.E_SERVICE_DISCONNECTED,
|
|
55
|
+
ErrorCode.E_BILLING_UNAVAILABLE,
|
|
56
|
+
ErrorCode.E_QUERY_PRODUCT,
|
|
57
|
+
ErrorCode.E_INIT_CONNECTION,
|
|
52
58
|
];
|
|
53
59
|
|
|
54
60
|
const errorCode = typeof error === 'string' ? error : error?.code;
|
|
@@ -68,20 +74,38 @@ export function getUserFriendlyErrorMessage(error: any): string {
|
|
|
68
74
|
return 'Purchase was cancelled by user';
|
|
69
75
|
case ErrorCode.E_NETWORK_ERROR:
|
|
70
76
|
return 'Network connection error. Please check your internet connection and try again.';
|
|
77
|
+
case ErrorCode.E_SERVICE_DISCONNECTED:
|
|
78
|
+
return 'Billing service disconnected. Please try again.';
|
|
79
|
+
case ErrorCode.E_BILLING_UNAVAILABLE:
|
|
80
|
+
return 'Billing is unavailable on this device or account.';
|
|
71
81
|
case ErrorCode.E_ITEM_UNAVAILABLE:
|
|
72
82
|
return 'This item is not available for purchase';
|
|
83
|
+
case ErrorCode.E_ITEM_NOT_OWNED:
|
|
84
|
+
return "You don't own this item";
|
|
73
85
|
case ErrorCode.E_ALREADY_OWNED:
|
|
74
86
|
return 'You already own this item';
|
|
87
|
+
case ErrorCode.E_SKU_NOT_FOUND:
|
|
88
|
+
return 'Requested product could not be found';
|
|
89
|
+
case ErrorCode.E_SKU_OFFER_MISMATCH:
|
|
90
|
+
return 'Selected offer does not match the SKU';
|
|
75
91
|
case ErrorCode.E_DEFERRED_PAYMENT:
|
|
76
92
|
return 'Payment is pending approval';
|
|
77
93
|
case ErrorCode.E_NOT_PREPARED:
|
|
78
94
|
return 'In-app purchase is not ready. Please try again later.';
|
|
79
95
|
case ErrorCode.E_SERVICE_ERROR:
|
|
80
96
|
return 'Store service error. Please try again later.';
|
|
97
|
+
case ErrorCode.E_FEATURE_NOT_SUPPORTED:
|
|
98
|
+
return 'This feature is not supported on this device.';
|
|
81
99
|
case ErrorCode.E_TRANSACTION_VALIDATION_FAILED:
|
|
82
100
|
return 'Transaction could not be verified';
|
|
83
101
|
case ErrorCode.E_RECEIPT_FAILED:
|
|
84
102
|
return 'Receipt processing failed';
|
|
103
|
+
case ErrorCode.E_EMPTY_SKU_LIST:
|
|
104
|
+
return 'No product IDs provided';
|
|
105
|
+
case ErrorCode.E_INIT_CONNECTION:
|
|
106
|
+
return 'Failed to initialize billing connection';
|
|
107
|
+
case ErrorCode.E_QUERY_PRODUCT:
|
|
108
|
+
return 'Failed to query products. Please try again later.';
|
|
85
109
|
default:
|
|
86
110
|
return error?.message || 'An unexpected error occurred';
|
|
87
111
|
}
|