expo-iap 3.0.2 → 3.0.4
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 +12 -0
- package/CLAUDE.md +2 -0
- package/build/helpers/subscription.d.ts +1 -12
- package/build/helpers/subscription.d.ts.map +1 -1
- package/build/helpers/subscription.js +12 -7
- package/build/helpers/subscription.js.map +1 -1
- package/build/index.d.ts +9 -7
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6 -4
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +7 -6
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +19 -4
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +7 -10
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +3 -1
- package/build/modules/ios.js.map +1 -1
- package/build/purchase-error.d.ts +69 -0
- package/build/purchase-error.d.ts.map +1 -0
- package/build/purchase-error.js +164 -0
- package/build/purchase-error.js.map +1 -0
- package/build/types.d.ts +649 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +100 -0
- package/build/types.js.map +1 -0
- package/build/useIAP.d.ts +6 -5
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +2 -3
- package/build/useIAP.js.map +1 -1
- package/build/utils/errorMapping.d.ts +10 -4
- package/build/utils/errorMapping.d.ts.map +1 -1
- package/build/utils/errorMapping.js +71 -47
- package/build/utils/errorMapping.js.map +1 -1
- package/jest.config.js +1 -1
- package/package.json +1 -1
- package/src/helpers/subscription.ts +12 -20
- package/src/index.ts +20 -20
- package/src/modules/android.ts +28 -10
- package/src/modules/ios.ts +11 -13
- package/src/purchase-error.ts +268 -0
- package/src/types.ts +738 -0
- package/src/useIAP.ts +15 -18
- package/src/utils/errorMapping.ts +89 -55
- package/build/ExpoIap.types.d.ts +0 -294
- package/build/ExpoIap.types.d.ts.map +0 -1
- package/build/ExpoIap.types.js +0 -226
- package/build/ExpoIap.types.js.map +0 -1
- package/build/types/ExpoIapAndroid.types.d.ts +0 -115
- package/build/types/ExpoIapAndroid.types.d.ts.map +0 -1
- package/build/types/ExpoIapAndroid.types.js +0 -29
- package/build/types/ExpoIapAndroid.types.js.map +0 -1
- package/build/types/ExpoIapIOS.types.d.ts +0 -143
- package/build/types/ExpoIapIOS.types.d.ts.map +0 -1
- package/build/types/ExpoIapIOS.types.js +0 -2
- package/build/types/ExpoIapIOS.types.js.map +0 -1
- package/src/ExpoIap.types.ts +0 -429
- package/src/types/ExpoIapAndroid.types.ts +0 -136
- package/src/types/ExpoIapIOS.types.ts +0 -167
package/jest.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import {Platform} from 'react-native';
|
|
2
2
|
import {getAvailablePurchases} from '../index';
|
|
3
|
-
|
|
4
|
-
export interface ActiveSubscription {
|
|
5
|
-
productId: string;
|
|
6
|
-
isActive: boolean;
|
|
7
|
-
transactionId: string; // Transaction identifier for backend validation
|
|
8
|
-
purchaseToken?: string; // JWT token (iOS) or purchase token (Android) for backend validation
|
|
9
|
-
transactionDate: number; // Transaction timestamp
|
|
10
|
-
expirationDateIOS?: Date;
|
|
11
|
-
autoRenewingAndroid?: boolean;
|
|
12
|
-
environmentIOS?: string;
|
|
13
|
-
willExpireSoon?: boolean;
|
|
14
|
-
daysUntilExpirationIOS?: number;
|
|
15
|
-
}
|
|
3
|
+
import type {ActiveSubscription} from '../types';
|
|
16
4
|
|
|
17
5
|
/**
|
|
18
6
|
* Get all active subscriptions with detailed information
|
|
@@ -91,8 +79,7 @@ export const getActiveSubscriptions = async (
|
|
|
91
79
|
const subscription: ActiveSubscription = {
|
|
92
80
|
productId: purchase.productId,
|
|
93
81
|
isActive: true,
|
|
94
|
-
|
|
95
|
-
transactionId: purchase.id,
|
|
82
|
+
transactionId: String(purchase.id),
|
|
96
83
|
purchaseToken: purchase.purchaseToken,
|
|
97
84
|
transactionDate: purchase.transactionDate,
|
|
98
85
|
};
|
|
@@ -100,8 +87,7 @@ export const getActiveSubscriptions = async (
|
|
|
100
87
|
// Add platform-specific details
|
|
101
88
|
if (Platform.OS === 'ios') {
|
|
102
89
|
if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
|
|
103
|
-
|
|
104
|
-
subscription.expirationDateIOS = expirationDate;
|
|
90
|
+
subscription.expirationDateIOS = purchase.expirationDateIOS;
|
|
105
91
|
|
|
106
92
|
// Calculate days until expiration (round to nearest day)
|
|
107
93
|
const daysUntilExpiration = Math.round(
|
|
@@ -112,13 +98,19 @@ export const getActiveSubscriptions = async (
|
|
|
112
98
|
}
|
|
113
99
|
|
|
114
100
|
if ('environmentIOS' in purchase) {
|
|
115
|
-
subscription.environmentIOS = purchase.environmentIOS;
|
|
101
|
+
subscription.environmentIOS = purchase.environmentIOS ?? undefined;
|
|
116
102
|
}
|
|
117
103
|
} else if (Platform.OS === 'android') {
|
|
118
104
|
if ('autoRenewingAndroid' in purchase) {
|
|
119
|
-
|
|
105
|
+
if (typeof purchase.autoRenewingAndroid !== 'undefined') {
|
|
106
|
+
subscription.autoRenewingAndroid = purchase.autoRenewingAndroid;
|
|
107
|
+
}
|
|
120
108
|
// If auto-renewing is false, subscription will expire soon
|
|
121
|
-
|
|
109
|
+
if (purchase.autoRenewingAndroid === false) {
|
|
110
|
+
subscription.willExpireSoon = true;
|
|
111
|
+
} else if (purchase.autoRenewingAndroid === true) {
|
|
112
|
+
subscription.willExpireSoon = false;
|
|
113
|
+
}
|
|
122
114
|
}
|
|
123
115
|
}
|
|
124
116
|
|
package/src/index.ts
CHANGED
|
@@ -20,19 +20,20 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
Product,
|
|
22
22
|
Purchase,
|
|
23
|
-
PurchaseError,
|
|
24
23
|
ErrorCode,
|
|
25
|
-
PurchaseResult,
|
|
26
|
-
RequestSubscriptionProps,
|
|
27
24
|
RequestPurchaseProps,
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
RequestSubscriptionPropsByPlatforms,
|
|
26
|
+
ProductSubscription,
|
|
30
27
|
PurchaseAndroid,
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
DiscountOfferInputIOS,
|
|
29
|
+
VoidResult,
|
|
30
|
+
ReceiptValidationResult,
|
|
31
|
+
} from './types';
|
|
32
|
+
import {PurchaseError} from './purchase-error';
|
|
33
33
|
|
|
34
34
|
// Export all types
|
|
35
|
-
export * from './
|
|
35
|
+
export * from './types';
|
|
36
|
+
export {ErrorCodeUtils, ErrorCodeMapping} from './purchase-error';
|
|
36
37
|
export * from './modules/android';
|
|
37
38
|
export * from './modules/ios';
|
|
38
39
|
|
|
@@ -40,7 +41,6 @@ export * from './modules/ios';
|
|
|
40
41
|
export {
|
|
41
42
|
getActiveSubscriptions,
|
|
42
43
|
hasActiveSubscriptions,
|
|
43
|
-
type ActiveSubscription,
|
|
44
44
|
} from './helpers/subscription';
|
|
45
45
|
|
|
46
46
|
// Get the native constant value
|
|
@@ -169,11 +169,11 @@ export const fetchProducts = async ({
|
|
|
169
169
|
}: {
|
|
170
170
|
skus: string[];
|
|
171
171
|
type?: 'inapp' | 'subs';
|
|
172
|
-
}): Promise<Product[] |
|
|
172
|
+
}): Promise<Product[] | ProductSubscription[]> => {
|
|
173
173
|
if (!skus?.length) {
|
|
174
174
|
throw new PurchaseError({
|
|
175
175
|
message: 'No SKUs provided',
|
|
176
|
-
code: ErrorCode.
|
|
176
|
+
code: ErrorCode.EmptySkuList,
|
|
177
177
|
});
|
|
178
178
|
}
|
|
179
179
|
|
|
@@ -195,7 +195,7 @@ export const fetchProducts = async ({
|
|
|
195
195
|
|
|
196
196
|
return type === 'inapp'
|
|
197
197
|
? (filteredItems as Product[])
|
|
198
|
-
: (filteredItems as
|
|
198
|
+
: (filteredItems as ProductSubscription[]);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
if (Platform.OS === 'android') {
|
|
@@ -213,7 +213,7 @@ export const fetchProducts = async ({
|
|
|
213
213
|
|
|
214
214
|
return type === 'inapp'
|
|
215
215
|
? (filteredItems as Product[])
|
|
216
|
-
: (filteredItems as
|
|
216
|
+
: (filteredItems as ProductSubscription[]);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
throw new Error('Unsupported platform');
|
|
@@ -272,8 +272,8 @@ export const restorePurchases = async (
|
|
|
272
272
|
};
|
|
273
273
|
|
|
274
274
|
const offerToRecordIOS = (
|
|
275
|
-
offer:
|
|
276
|
-
): Record<keyof
|
|
275
|
+
offer: DiscountOfferInputIOS | undefined,
|
|
276
|
+
): Record<keyof DiscountOfferInputIOS, string> | undefined => {
|
|
277
277
|
if (!offer) return undefined;
|
|
278
278
|
return {
|
|
279
279
|
identifier: offer.identifier,
|
|
@@ -291,7 +291,7 @@ type PurchaseRequest =
|
|
|
291
291
|
type?: 'inapp';
|
|
292
292
|
}
|
|
293
293
|
| {
|
|
294
|
-
request:
|
|
294
|
+
request: RequestSubscriptionPropsByPlatforms;
|
|
295
295
|
type: 'subs';
|
|
296
296
|
};
|
|
297
297
|
|
|
@@ -299,7 +299,7 @@ type PurchaseRequest =
|
|
|
299
299
|
* Helper to normalize request props to platform-specific format
|
|
300
300
|
*/
|
|
301
301
|
const normalizeRequestProps = (
|
|
302
|
-
request: RequestPurchaseProps |
|
|
302
|
+
request: RequestPurchaseProps | RequestSubscriptionPropsByPlatforms,
|
|
303
303
|
platform: 'ios' | 'android',
|
|
304
304
|
): any => {
|
|
305
305
|
// Platform-specific format - directly return the appropriate platform data
|
|
@@ -443,7 +443,7 @@ export const finishTransaction = ({
|
|
|
443
443
|
}: {
|
|
444
444
|
purchase: Purchase;
|
|
445
445
|
isConsumable?: boolean;
|
|
446
|
-
}): Promise<
|
|
446
|
+
}): Promise<VoidResult | boolean> => {
|
|
447
447
|
return (
|
|
448
448
|
Platform.select({
|
|
449
449
|
ios: async () => {
|
|
@@ -466,7 +466,7 @@ export const finishTransaction = ({
|
|
|
466
466
|
return Promise.reject(
|
|
467
467
|
new PurchaseError({
|
|
468
468
|
message: 'Purchase token is required to finish transaction',
|
|
469
|
-
code:
|
|
469
|
+
code: ErrorCode.DeveloperError,
|
|
470
470
|
productId: androidPurchase.productId,
|
|
471
471
|
platform: 'android',
|
|
472
472
|
}),
|
|
@@ -539,7 +539,7 @@ export const validateReceipt = async (
|
|
|
539
539
|
accessToken: string;
|
|
540
540
|
isSub?: boolean;
|
|
541
541
|
},
|
|
542
|
-
): Promise<
|
|
542
|
+
): Promise<ReceiptValidationResult> => {
|
|
543
543
|
if (Platform.OS === 'ios') {
|
|
544
544
|
return await validateReceiptIOS(sku);
|
|
545
545
|
} else if (Platform.OS === 'android') {
|
package/src/modules/android.ts
CHANGED
|
@@ -5,17 +5,19 @@ import {Linking} from 'react-native';
|
|
|
5
5
|
import ExpoIapModule from '../ExpoIapModule';
|
|
6
6
|
|
|
7
7
|
// Types
|
|
8
|
-
import type {
|
|
8
|
+
import type {ReceiptValidationResultAndroid, VoidResult} from '../types';
|
|
9
|
+
import {Platform as PurchasePlatform} from '../types';
|
|
9
10
|
|
|
10
11
|
// Type guards
|
|
11
|
-
export function isProductAndroid<
|
|
12
|
-
|
|
13
|
-
): item is T & {platform: 'android'} {
|
|
12
|
+
export function isProductAndroid<
|
|
13
|
+
T extends {platform?: string | PurchasePlatform},
|
|
14
|
+
>(item: unknown): item is T & {platform: PurchasePlatform.Android | 'android'} {
|
|
14
15
|
return (
|
|
15
16
|
item != null &&
|
|
16
17
|
typeof item === 'object' &&
|
|
17
18
|
'platform' in item &&
|
|
18
|
-
item.platform === 'android'
|
|
19
|
+
((item as any).platform === 'android' ||
|
|
20
|
+
(item as any).platform === PurchasePlatform.Android)
|
|
19
21
|
);
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -86,7 +88,7 @@ export const validateReceiptAndroid = async ({
|
|
|
86
88
|
productToken: string;
|
|
87
89
|
accessToken: string;
|
|
88
90
|
isSub?: boolean;
|
|
89
|
-
}): Promise<
|
|
91
|
+
}): Promise<ReceiptValidationResultAndroid> => {
|
|
90
92
|
const type = isSub ? 'subscriptions' : 'products';
|
|
91
93
|
|
|
92
94
|
const url =
|
|
@@ -115,14 +117,30 @@ export const validateReceiptAndroid = async ({
|
|
|
115
117
|
* Acknowledge a product (on Android.) No-op on iOS.
|
|
116
118
|
* @param {Object} params - The parameters object
|
|
117
119
|
* @param {string} params.token - The product's token (on Android)
|
|
118
|
-
* @returns {Promise<
|
|
120
|
+
* @returns {Promise<VoidResult | void>}
|
|
119
121
|
*/
|
|
120
|
-
export const acknowledgePurchaseAndroid = ({
|
|
122
|
+
export const acknowledgePurchaseAndroid = async ({
|
|
121
123
|
token,
|
|
122
124
|
}: {
|
|
123
125
|
token: string;
|
|
124
|
-
}): Promise<
|
|
125
|
-
|
|
126
|
+
}): Promise<VoidResult | boolean | void> => {
|
|
127
|
+
const result = await ExpoIapModule.acknowledgePurchaseAndroid(token);
|
|
128
|
+
|
|
129
|
+
if (typeof result === 'boolean' || typeof result === 'undefined') {
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (result && typeof result === 'object') {
|
|
134
|
+
const record = result as Record<string, unknown>;
|
|
135
|
+
if (typeof record.success === 'boolean') {
|
|
136
|
+
return {success: record.success};
|
|
137
|
+
}
|
|
138
|
+
if (typeof record.responseCode === 'number') {
|
|
139
|
+
return {success: record.responseCode === 0};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {success: true};
|
|
126
144
|
};
|
|
127
145
|
|
|
128
146
|
/**
|
package/src/modules/ios.ts
CHANGED
|
@@ -8,10 +8,12 @@ import ExpoIapModule from '../ExpoIapModule';
|
|
|
8
8
|
import type {
|
|
9
9
|
Product,
|
|
10
10
|
Purchase,
|
|
11
|
-
PurchaseError,
|
|
12
11
|
SubscriptionStatusIOS,
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
AppTransaction,
|
|
13
|
+
ReceiptValidationResultIOS,
|
|
14
|
+
} from '../types';
|
|
15
|
+
import type {PurchaseError} from '../purchase-error';
|
|
16
|
+
import {Platform as PurchasePlatform} from '../types';
|
|
15
17
|
import {Linking} from 'react-native';
|
|
16
18
|
|
|
17
19
|
export type TransactionEvent = {
|
|
@@ -22,14 +24,15 @@ export type TransactionEvent = {
|
|
|
22
24
|
// Listeners
|
|
23
25
|
|
|
24
26
|
// Type guards
|
|
25
|
-
export function isProductIOS<T extends {platform?: string}>(
|
|
27
|
+
export function isProductIOS<T extends {platform?: string | PurchasePlatform}>(
|
|
26
28
|
item: unknown,
|
|
27
|
-
): item is T & {platform: 'ios'} {
|
|
29
|
+
): item is T & {platform: PurchasePlatform.Ios | 'ios'} {
|
|
28
30
|
return (
|
|
29
31
|
item != null &&
|
|
30
32
|
typeof item === 'object' &&
|
|
31
33
|
'platform' in item &&
|
|
32
|
-
item.platform === 'ios'
|
|
34
|
+
((item as any).platform === 'ios' ||
|
|
35
|
+
(item as any).platform === PurchasePlatform.Ios)
|
|
33
36
|
);
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -191,12 +194,7 @@ export const getTransactionJwsIOS = (sku: string): Promise<string> => {
|
|
|
191
194
|
*/
|
|
192
195
|
export const validateReceiptIOS = async (
|
|
193
196
|
sku: string,
|
|
194
|
-
): Promise<{
|
|
195
|
-
isValid: boolean;
|
|
196
|
-
receiptData: string;
|
|
197
|
-
jwsRepresentation: string;
|
|
198
|
-
latestTransaction?: Purchase;
|
|
199
|
-
}> => {
|
|
197
|
+
): Promise<ReceiptValidationResultIOS> => {
|
|
200
198
|
const result = await ExpoIapModule.validateReceiptIOS(sku);
|
|
201
199
|
return result;
|
|
202
200
|
};
|
|
@@ -230,7 +228,7 @@ export const presentCodeRedemptionSheetIOS = (): Promise<boolean> => {
|
|
|
230
228
|
* @platform iOS
|
|
231
229
|
* @since iOS 16.0
|
|
232
230
|
*/
|
|
233
|
-
export const getAppTransactionIOS = (): Promise<
|
|
231
|
+
export const getAppTransactionIOS = (): Promise<AppTransaction | null> => {
|
|
234
232
|
return ExpoIapModule.getAppTransactionIOS();
|
|
235
233
|
};
|
|
236
234
|
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {NATIVE_ERROR_CODES} from './ExpoIapModule';
|
|
2
|
+
import {ErrorCode, Platform} from './types';
|
|
3
|
+
|
|
4
|
+
/** Platform identifiers supported by {@link PurchaseError}. */
|
|
5
|
+
export type PurchaseErrorPlatform = Platform | 'ios' | 'android';
|
|
6
|
+
|
|
7
|
+
/** Properties used to construct a {@link PurchaseError}. */
|
|
8
|
+
export interface PurchaseErrorProps {
|
|
9
|
+
message: string;
|
|
10
|
+
responseCode?: number;
|
|
11
|
+
debugMessage?: string;
|
|
12
|
+
code?: ErrorCode;
|
|
13
|
+
productId?: string;
|
|
14
|
+
platform?: PurchaseErrorPlatform;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Shape of raw platform error objects coming from native modules. */
|
|
18
|
+
type PlatformErrorData = {
|
|
19
|
+
code?: string | number;
|
|
20
|
+
message?: string;
|
|
21
|
+
responseCode?: number;
|
|
22
|
+
debugMessage?: string;
|
|
23
|
+
productId?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const toStandardizedCode = (errorCode: ErrorCode): string =>
|
|
27
|
+
errorCode.startsWith('E_') ? errorCode : `E_${errorCode}`;
|
|
28
|
+
|
|
29
|
+
const normalizePlatform = (
|
|
30
|
+
platform: PurchaseErrorPlatform,
|
|
31
|
+
): 'ios' | 'android' =>
|
|
32
|
+
platform === Platform.Ios || platform === 'ios' ? 'ios' : 'android';
|
|
33
|
+
|
|
34
|
+
const OPENIAP_ERROR_CODE_SET: Set<string> = new Set(
|
|
35
|
+
Object.values(ErrorCode).map((code) => toStandardizedCode(code)),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const COMMON_ERROR_CODE_MAP: Record<ErrorCode, string> = {
|
|
39
|
+
[ErrorCode.Unknown]: toStandardizedCode(ErrorCode.Unknown),
|
|
40
|
+
[ErrorCode.UserCancelled]: toStandardizedCode(ErrorCode.UserCancelled),
|
|
41
|
+
[ErrorCode.UserError]: toStandardizedCode(ErrorCode.UserError),
|
|
42
|
+
[ErrorCode.ItemUnavailable]: toStandardizedCode(ErrorCode.ItemUnavailable),
|
|
43
|
+
[ErrorCode.RemoteError]: toStandardizedCode(ErrorCode.RemoteError),
|
|
44
|
+
[ErrorCode.NetworkError]: toStandardizedCode(ErrorCode.NetworkError),
|
|
45
|
+
[ErrorCode.ServiceError]: toStandardizedCode(ErrorCode.ServiceError),
|
|
46
|
+
[ErrorCode.ReceiptFailed]: toStandardizedCode(ErrorCode.ReceiptFailed),
|
|
47
|
+
[ErrorCode.ReceiptFinished]: toStandardizedCode(ErrorCode.ReceiptFinished),
|
|
48
|
+
[ErrorCode.ReceiptFinishedFailed]: toStandardizedCode(
|
|
49
|
+
ErrorCode.ReceiptFinishedFailed,
|
|
50
|
+
),
|
|
51
|
+
[ErrorCode.NotPrepared]: toStandardizedCode(ErrorCode.NotPrepared),
|
|
52
|
+
[ErrorCode.NotEnded]: toStandardizedCode(ErrorCode.NotEnded),
|
|
53
|
+
[ErrorCode.AlreadyOwned]: toStandardizedCode(ErrorCode.AlreadyOwned),
|
|
54
|
+
[ErrorCode.DeveloperError]: toStandardizedCode(ErrorCode.DeveloperError),
|
|
55
|
+
[ErrorCode.BillingResponseJsonParseError]: toStandardizedCode(
|
|
56
|
+
ErrorCode.BillingResponseJsonParseError,
|
|
57
|
+
),
|
|
58
|
+
[ErrorCode.DeferredPayment]: toStandardizedCode(ErrorCode.DeferredPayment),
|
|
59
|
+
[ErrorCode.Interrupted]: toStandardizedCode(ErrorCode.Interrupted),
|
|
60
|
+
[ErrorCode.IapNotAvailable]: toStandardizedCode(ErrorCode.IapNotAvailable),
|
|
61
|
+
[ErrorCode.PurchaseError]: toStandardizedCode(ErrorCode.PurchaseError),
|
|
62
|
+
[ErrorCode.SyncError]: toStandardizedCode(ErrorCode.SyncError),
|
|
63
|
+
[ErrorCode.TransactionValidationFailed]: toStandardizedCode(
|
|
64
|
+
ErrorCode.TransactionValidationFailed,
|
|
65
|
+
),
|
|
66
|
+
[ErrorCode.ActivityUnavailable]: toStandardizedCode(
|
|
67
|
+
ErrorCode.ActivityUnavailable,
|
|
68
|
+
),
|
|
69
|
+
[ErrorCode.AlreadyPrepared]: toStandardizedCode(ErrorCode.AlreadyPrepared),
|
|
70
|
+
[ErrorCode.Pending]: toStandardizedCode(ErrorCode.Pending),
|
|
71
|
+
[ErrorCode.ConnectionClosed]: toStandardizedCode(ErrorCode.ConnectionClosed),
|
|
72
|
+
[ErrorCode.InitConnection]: toStandardizedCode(ErrorCode.InitConnection),
|
|
73
|
+
[ErrorCode.ServiceDisconnected]: toStandardizedCode(
|
|
74
|
+
ErrorCode.ServiceDisconnected,
|
|
75
|
+
),
|
|
76
|
+
[ErrorCode.QueryProduct]: toStandardizedCode(ErrorCode.QueryProduct),
|
|
77
|
+
[ErrorCode.SkuNotFound]: toStandardizedCode(ErrorCode.SkuNotFound),
|
|
78
|
+
[ErrorCode.SkuOfferMismatch]: toStandardizedCode(ErrorCode.SkuOfferMismatch),
|
|
79
|
+
[ErrorCode.ItemNotOwned]: toStandardizedCode(ErrorCode.ItemNotOwned),
|
|
80
|
+
[ErrorCode.BillingUnavailable]: toStandardizedCode(
|
|
81
|
+
ErrorCode.BillingUnavailable,
|
|
82
|
+
),
|
|
83
|
+
[ErrorCode.FeatureNotSupported]: toStandardizedCode(
|
|
84
|
+
ErrorCode.FeatureNotSupported,
|
|
85
|
+
),
|
|
86
|
+
[ErrorCode.EmptySkuList]: toStandardizedCode(ErrorCode.EmptySkuList),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Mapping between platforms and their canonical error codes.
|
|
91
|
+
* Values are platform-native string identifiers.
|
|
92
|
+
*/
|
|
93
|
+
export const ErrorCodeMapping = {
|
|
94
|
+
ios: COMMON_ERROR_CODE_MAP,
|
|
95
|
+
android: COMMON_ERROR_CODE_MAP,
|
|
96
|
+
} as const;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Error thrown by expo-iap when purchases fail.
|
|
100
|
+
*/
|
|
101
|
+
export class PurchaseError extends Error {
|
|
102
|
+
public responseCode?: number;
|
|
103
|
+
public debugMessage?: string;
|
|
104
|
+
public code?: ErrorCode;
|
|
105
|
+
public productId?: string;
|
|
106
|
+
public platform?: PurchaseErrorPlatform;
|
|
107
|
+
|
|
108
|
+
constructor(
|
|
109
|
+
message: string,
|
|
110
|
+
responseCode?: number,
|
|
111
|
+
debugMessage?: string,
|
|
112
|
+
code?: ErrorCode,
|
|
113
|
+
productId?: string,
|
|
114
|
+
platform?: PurchaseErrorPlatform,
|
|
115
|
+
);
|
|
116
|
+
constructor(props: PurchaseErrorProps);
|
|
117
|
+
constructor(
|
|
118
|
+
messageOrProps: string | PurchaseErrorProps,
|
|
119
|
+
responseCode?: number,
|
|
120
|
+
debugMessage?: string,
|
|
121
|
+
code?: ErrorCode,
|
|
122
|
+
productId?: string,
|
|
123
|
+
platform?: PurchaseErrorPlatform,
|
|
124
|
+
) {
|
|
125
|
+
super(
|
|
126
|
+
typeof messageOrProps === 'string'
|
|
127
|
+
? messageOrProps
|
|
128
|
+
: messageOrProps.message,
|
|
129
|
+
);
|
|
130
|
+
this.name = '[expo-iap]: PurchaseError';
|
|
131
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
132
|
+
|
|
133
|
+
if (typeof messageOrProps === 'string') {
|
|
134
|
+
this.responseCode = responseCode;
|
|
135
|
+
this.debugMessage = debugMessage;
|
|
136
|
+
this.code = code;
|
|
137
|
+
this.productId = productId;
|
|
138
|
+
this.platform = platform;
|
|
139
|
+
} else {
|
|
140
|
+
this.responseCode = messageOrProps.responseCode;
|
|
141
|
+
this.debugMessage = messageOrProps.debugMessage;
|
|
142
|
+
this.code = messageOrProps.code;
|
|
143
|
+
this.productId = messageOrProps.productId;
|
|
144
|
+
this.platform = messageOrProps.platform;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a {@link PurchaseError} from raw platform error data.
|
|
150
|
+
*/
|
|
151
|
+
static fromPlatformError(
|
|
152
|
+
errorData: PlatformErrorData,
|
|
153
|
+
platform: PurchaseErrorPlatform,
|
|
154
|
+
): PurchaseError {
|
|
155
|
+
const normalizedPlatform = normalizePlatform(platform);
|
|
156
|
+
|
|
157
|
+
const errorCode = errorData.code
|
|
158
|
+
? ErrorCodeUtils.fromPlatformCode(errorData.code, normalizedPlatform)
|
|
159
|
+
: ErrorCode.Unknown;
|
|
160
|
+
|
|
161
|
+
return new PurchaseError({
|
|
162
|
+
message: errorData.message ?? 'Unknown error occurred',
|
|
163
|
+
responseCode: errorData.responseCode,
|
|
164
|
+
debugMessage: errorData.debugMessage,
|
|
165
|
+
code: errorCode,
|
|
166
|
+
productId: errorData.productId,
|
|
167
|
+
platform,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Returns the platform specific error code for this instance.
|
|
173
|
+
*/
|
|
174
|
+
getPlatformCode(): string | number | undefined {
|
|
175
|
+
if (!this.code || !this.platform) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
return ErrorCodeUtils.toPlatformCode(this.code, this.platform);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Utility helpers for translating error codes between platforms. */
|
|
183
|
+
export const ErrorCodeUtils = {
|
|
184
|
+
/**
|
|
185
|
+
* Returns the native error code for the provided {@link ErrorCode}.
|
|
186
|
+
*/
|
|
187
|
+
getNativeErrorCode: (errorCode: ErrorCode): string => {
|
|
188
|
+
const standardized = toStandardizedCode(errorCode);
|
|
189
|
+
return (
|
|
190
|
+
(NATIVE_ERROR_CODES as Record<string, string | undefined>)[
|
|
191
|
+
standardized
|
|
192
|
+
] ?? standardized
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
/**
|
|
196
|
+
* Converts a platform-specific error code into a standardized {@link ErrorCode}.
|
|
197
|
+
*/
|
|
198
|
+
fromPlatformCode: (
|
|
199
|
+
platformCode: string | number,
|
|
200
|
+
_platform: PurchaseErrorPlatform,
|
|
201
|
+
): ErrorCode => {
|
|
202
|
+
if (typeof platformCode === 'string' && platformCode.startsWith('E_')) {
|
|
203
|
+
if (OPENIAP_ERROR_CODE_SET.has(platformCode)) {
|
|
204
|
+
const match = Object.entries(COMMON_ERROR_CODE_MAP).find(
|
|
205
|
+
([, value]) => value === platformCode,
|
|
206
|
+
);
|
|
207
|
+
if (match) {
|
|
208
|
+
return match[0] as ErrorCode;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const [standardized, nativeCode] of Object.entries(
|
|
214
|
+
(NATIVE_ERROR_CODES || {}) as Record<string, string | number>,
|
|
215
|
+
)) {
|
|
216
|
+
if (
|
|
217
|
+
nativeCode === platformCode &&
|
|
218
|
+
OPENIAP_ERROR_CODE_SET.has(standardized)
|
|
219
|
+
) {
|
|
220
|
+
const match = Object.entries(COMMON_ERROR_CODE_MAP).find(
|
|
221
|
+
([, mappedCode]) => mappedCode === standardized,
|
|
222
|
+
);
|
|
223
|
+
if (match) {
|
|
224
|
+
return match[0] as ErrorCode;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const [errorCode, mappedCode] of Object.entries(
|
|
230
|
+
COMMON_ERROR_CODE_MAP,
|
|
231
|
+
)) {
|
|
232
|
+
if (mappedCode === platformCode) {
|
|
233
|
+
return errorCode as ErrorCode;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return ErrorCode.Unknown;
|
|
238
|
+
},
|
|
239
|
+
/**
|
|
240
|
+
* Converts a standardized {@link ErrorCode} into its platform-specific value.
|
|
241
|
+
*/
|
|
242
|
+
toPlatformCode: (
|
|
243
|
+
errorCode: ErrorCode,
|
|
244
|
+
_platform: PurchaseErrorPlatform,
|
|
245
|
+
): string | number => {
|
|
246
|
+
const standardized = toStandardizedCode(errorCode);
|
|
247
|
+
const native = (NATIVE_ERROR_CODES as Record<string, string | number>)[
|
|
248
|
+
standardized
|
|
249
|
+
];
|
|
250
|
+
return native ?? COMMON_ERROR_CODE_MAP[errorCode] ?? 'E_UNKNOWN';
|
|
251
|
+
},
|
|
252
|
+
/**
|
|
253
|
+
* Determines whether the error code is supported on the given platform.
|
|
254
|
+
*/
|
|
255
|
+
isValidForPlatform: (
|
|
256
|
+
errorCode: ErrorCode,
|
|
257
|
+
platform: PurchaseErrorPlatform,
|
|
258
|
+
): boolean => {
|
|
259
|
+
const standardized = toStandardizedCode(errorCode);
|
|
260
|
+
if (
|
|
261
|
+
(NATIVE_ERROR_CODES as Record<string, unknown>)[standardized] !==
|
|
262
|
+
undefined
|
|
263
|
+
) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
return standardized in ErrorCodeMapping[normalizePlatform(platform)];
|
|
267
|
+
},
|
|
268
|
+
};
|