expo-iap 2.9.0 → 2.9.1
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 +16 -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 +0 -13
- package/build/index.d.ts.map +1 -1
- package/build/index.js +22 -22
- 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 +35 -26
- 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 +43 -30
- package/package.json +1 -1
- package/src/ExpoIap.types.ts +84 -37
- package/src/index.ts +25 -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 +41 -34
- package/src/utils/errorMapping.ts +24 -0
package/src/ExpoIap.types.ts
CHANGED
|
@@ -70,15 +70,7 @@ export type Purchase =
|
|
|
70
70
|
| (PurchaseAndroid & AndroidPlatform)
|
|
71
71
|
| (PurchaseIOS & IosPlatform);
|
|
72
72
|
|
|
73
|
-
//
|
|
74
|
-
/**
|
|
75
|
-
* @deprecated Use `Purchase` instead. This type alias will be removed in v2.9.0.
|
|
76
|
-
*/
|
|
77
|
-
export type ProductPurchase = Purchase;
|
|
78
|
-
/**
|
|
79
|
-
* @deprecated Use `Purchase` instead. This type alias will be removed in v2.9.0.
|
|
80
|
-
*/
|
|
81
|
-
export type SubscriptionPurchase = Purchase;
|
|
73
|
+
// Removed legacy type aliases `ProductPurchase` and `SubscriptionPurchase` in v2.9.0
|
|
82
74
|
|
|
83
75
|
export type PurchaseResult = {
|
|
84
76
|
responseCode?: number;
|
|
@@ -120,6 +112,16 @@ export enum ErrorCode {
|
|
|
120
112
|
E_ALREADY_PREPARED = 'E_ALREADY_PREPARED',
|
|
121
113
|
E_PENDING = 'E_PENDING',
|
|
122
114
|
E_CONNECTION_CLOSED = 'E_CONNECTION_CLOSED',
|
|
115
|
+
// Additional detailed errors (Android-focused, kept cross-platform)
|
|
116
|
+
E_INIT_CONNECTION = 'E_INIT_CONNECTION',
|
|
117
|
+
E_SERVICE_DISCONNECTED = 'E_SERVICE_DISCONNECTED',
|
|
118
|
+
E_QUERY_PRODUCT = 'E_QUERY_PRODUCT',
|
|
119
|
+
E_SKU_NOT_FOUND = 'E_SKU_NOT_FOUND',
|
|
120
|
+
E_SKU_OFFER_MISMATCH = 'E_SKU_OFFER_MISMATCH',
|
|
121
|
+
E_ITEM_NOT_OWNED = 'E_ITEM_NOT_OWNED',
|
|
122
|
+
E_BILLING_UNAVAILABLE = 'E_BILLING_UNAVAILABLE',
|
|
123
|
+
E_FEATURE_NOT_SUPPORTED = 'E_FEATURE_NOT_SUPPORTED',
|
|
124
|
+
E_EMPTY_SKU_LIST = 'E_EMPTY_SKU_LIST',
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
/**
|
|
@@ -180,26 +182,59 @@ export const ErrorCodeMapping = {
|
|
|
180
182
|
[ErrorCode.E_ALREADY_PREPARED]: 'E_ALREADY_PREPARED',
|
|
181
183
|
[ErrorCode.E_PENDING]: 'E_PENDING',
|
|
182
184
|
[ErrorCode.E_CONNECTION_CLOSED]: 'E_CONNECTION_CLOSED',
|
|
185
|
+
[ErrorCode.E_INIT_CONNECTION]: 'E_INIT_CONNECTION',
|
|
186
|
+
[ErrorCode.E_SERVICE_DISCONNECTED]: 'E_SERVICE_DISCONNECTED',
|
|
187
|
+
[ErrorCode.E_QUERY_PRODUCT]: 'E_QUERY_PRODUCT',
|
|
188
|
+
[ErrorCode.E_SKU_NOT_FOUND]: 'E_SKU_NOT_FOUND',
|
|
189
|
+
[ErrorCode.E_SKU_OFFER_MISMATCH]: 'E_SKU_OFFER_MISMATCH',
|
|
190
|
+
[ErrorCode.E_ITEM_NOT_OWNED]: 'E_ITEM_NOT_OWNED',
|
|
191
|
+
[ErrorCode.E_BILLING_UNAVAILABLE]: 'E_BILLING_UNAVAILABLE',
|
|
192
|
+
[ErrorCode.E_FEATURE_NOT_SUPPORTED]: 'E_FEATURE_NOT_SUPPORTED',
|
|
193
|
+
[ErrorCode.E_EMPTY_SKU_LIST]: 'E_EMPTY_SKU_LIST',
|
|
183
194
|
},
|
|
184
195
|
} as const;
|
|
185
196
|
|
|
197
|
+
export type PurchaseErrorProps = {
|
|
198
|
+
message: string;
|
|
199
|
+
responseCode?: number;
|
|
200
|
+
debugMessage?: string;
|
|
201
|
+
code?: ErrorCode;
|
|
202
|
+
productId?: string;
|
|
203
|
+
platform?: 'ios' | 'android';
|
|
204
|
+
};
|
|
205
|
+
|
|
186
206
|
export class PurchaseError implements Error {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
207
|
+
public name: string;
|
|
208
|
+
public message: string;
|
|
209
|
+
public responseCode?: number;
|
|
210
|
+
public debugMessage?: string;
|
|
211
|
+
public code?: ErrorCode;
|
|
212
|
+
public productId?: string;
|
|
213
|
+
public platform?: 'ios' | 'android';
|
|
214
|
+
|
|
215
|
+
// Backwards-compatible constructor: accepts either props object or legacy positional args
|
|
216
|
+
constructor(messageOrProps: string | PurchaseErrorProps, ...rest: any[]) {
|
|
196
217
|
this.name = '[expo-iap]: PurchaseError';
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
218
|
+
|
|
219
|
+
if (typeof messageOrProps === 'string') {
|
|
220
|
+
// Legacy signature: (name, message, responseCode?, debugMessage?, code?, productId?, platform?)
|
|
221
|
+
// The first legacy argument was a name which we always override, so treat it as message here
|
|
222
|
+
const message = messageOrProps;
|
|
223
|
+
this.message = message;
|
|
224
|
+
this.responseCode = rest[0];
|
|
225
|
+
this.debugMessage = rest[1];
|
|
226
|
+
this.code = rest[2];
|
|
227
|
+
this.productId = rest[3];
|
|
228
|
+
this.platform = rest[4];
|
|
229
|
+
} else {
|
|
230
|
+
const props = messageOrProps;
|
|
231
|
+
this.message = props.message;
|
|
232
|
+
this.responseCode = props.responseCode;
|
|
233
|
+
this.debugMessage = props.debugMessage;
|
|
234
|
+
this.code = props.code;
|
|
235
|
+
this.productId = props.productId;
|
|
236
|
+
this.platform = props.platform;
|
|
237
|
+
}
|
|
203
238
|
}
|
|
204
239
|
|
|
205
240
|
/**
|
|
@@ -216,15 +251,14 @@ export class PurchaseError implements Error {
|
|
|
216
251
|
? ErrorCodeUtils.fromPlatformCode(errorData.code, platform)
|
|
217
252
|
: ErrorCode.E_UNKNOWN;
|
|
218
253
|
|
|
219
|
-
return new PurchaseError(
|
|
220
|
-
|
|
221
|
-
errorData.
|
|
222
|
-
errorData.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
errorData.productId,
|
|
254
|
+
return new PurchaseError({
|
|
255
|
+
message: errorData.message || 'Unknown error occurred',
|
|
256
|
+
responseCode: errorData.responseCode,
|
|
257
|
+
debugMessage: errorData.debugMessage,
|
|
258
|
+
code: errorCode,
|
|
259
|
+
productId: errorData.productId,
|
|
226
260
|
platform,
|
|
227
|
-
);
|
|
261
|
+
});
|
|
228
262
|
}
|
|
229
263
|
|
|
230
264
|
/**
|
|
@@ -260,8 +294,16 @@ export const ErrorCodeUtils = {
|
|
|
260
294
|
platformCode: string | number,
|
|
261
295
|
platform: 'ios' | 'android',
|
|
262
296
|
): ErrorCode => {
|
|
263
|
-
|
|
297
|
+
// Prefer dynamic native mapping for iOS to avoid drift
|
|
298
|
+
if (platform === 'ios') {
|
|
299
|
+
for (const [key, value] of Object.entries(NATIVE_ERROR_CODES || {})) {
|
|
300
|
+
if (value === platformCode) {
|
|
301
|
+
return key as ErrorCode;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
264
305
|
|
|
306
|
+
const mapping = ErrorCodeMapping[platform];
|
|
265
307
|
for (const [errorCode, mappedCode] of Object.entries(mapping)) {
|
|
266
308
|
if (mappedCode === platformCode) {
|
|
267
309
|
return errorCode as ErrorCode;
|
|
@@ -281,10 +323,15 @@ export const ErrorCodeUtils = {
|
|
|
281
323
|
errorCode: ErrorCode,
|
|
282
324
|
platform: 'ios' | 'android',
|
|
283
325
|
): string | number => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
(
|
|
287
|
-
|
|
326
|
+
if (platform === 'ios') {
|
|
327
|
+
const native = NATIVE_ERROR_CODES?.[errorCode];
|
|
328
|
+
if (native !== undefined) return native;
|
|
329
|
+
}
|
|
330
|
+
const mapping = ErrorCodeMapping[platform] as Record<
|
|
331
|
+
ErrorCode,
|
|
332
|
+
string | number
|
|
333
|
+
>;
|
|
334
|
+
return mapping[errorCode] ?? (platform === 'ios' ? 0 : 'E_UNKNOWN');
|
|
288
335
|
},
|
|
289
336
|
|
|
290
337
|
/**
|
package/src/index.ts
CHANGED
|
@@ -143,7 +143,12 @@ export const getProducts = async (skus: string[]): Promise<Product[]> => {
|
|
|
143
143
|
"`getProducts` is deprecated. Use `fetchProducts({ skus, type: 'inapp' })` instead. This function will be removed in version 3.0.0.",
|
|
144
144
|
);
|
|
145
145
|
if (!skus?.length) {
|
|
146
|
-
return Promise.reject(
|
|
146
|
+
return Promise.reject(
|
|
147
|
+
new PurchaseError({
|
|
148
|
+
message: 'No SKUs provided',
|
|
149
|
+
code: ErrorCode.E_EMPTY_SKU_LIST,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
return Platform.select({
|
|
@@ -177,7 +182,12 @@ export const getSubscriptions = async (
|
|
|
177
182
|
"`getSubscriptions` is deprecated. Use `fetchProducts({ skus, type: 'subs' })` instead. This function will be removed in version 3.0.0.",
|
|
178
183
|
);
|
|
179
184
|
if (!skus?.length) {
|
|
180
|
-
return Promise.reject(
|
|
185
|
+
return Promise.reject(
|
|
186
|
+
new PurchaseError({
|
|
187
|
+
message: 'No SKUs provided',
|
|
188
|
+
code: ErrorCode.E_EMPTY_SKU_LIST,
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
return Platform.select({
|
|
@@ -245,7 +255,10 @@ export const fetchProducts = async ({
|
|
|
245
255
|
type?: 'inapp' | 'subs';
|
|
246
256
|
}): Promise<Product[] | SubscriptionProduct[]> => {
|
|
247
257
|
if (!skus?.length) {
|
|
248
|
-
throw new
|
|
258
|
+
throw new PurchaseError({
|
|
259
|
+
message: 'No SKUs provided',
|
|
260
|
+
code: ErrorCode.E_EMPTY_SKU_LIST,
|
|
261
|
+
});
|
|
249
262
|
}
|
|
250
263
|
|
|
251
264
|
if (Platform.OS === 'ios') {
|
|
@@ -344,7 +357,8 @@ export const getPurchaseHistory = ({
|
|
|
344
357
|
console.warn(
|
|
345
358
|
'`getPurchaseHistory` is deprecated. Use `getPurchaseHistories` instead. This function will be removed in version 3.0.0.',
|
|
346
359
|
);
|
|
347
|
-
|
|
360
|
+
// Use available purchases as a best-effort replacement
|
|
361
|
+
return getAvailablePurchases({
|
|
348
362
|
alsoPublishToEventListenerIOS:
|
|
349
363
|
alsoPublishToEventListenerIOS ?? alsoPublishToEventListener,
|
|
350
364
|
onlyIncludeActiveItemsIOS:
|
|
@@ -352,42 +366,7 @@ export const getPurchaseHistory = ({
|
|
|
352
366
|
});
|
|
353
367
|
};
|
|
354
368
|
|
|
355
|
-
|
|
356
|
-
* @deprecated Use getAvailablePurchases instead. This function is just calling getAvailablePurchases internally on iOS
|
|
357
|
-
* and returns an empty array on Android (Google Play Billing v8 removed purchase history API).
|
|
358
|
-
* Will be removed in v2.9.0
|
|
359
|
-
*/
|
|
360
|
-
export const getPurchaseHistories = ({
|
|
361
|
-
alsoPublishToEventListener = false,
|
|
362
|
-
onlyIncludeActiveItems = false,
|
|
363
|
-
alsoPublishToEventListenerIOS,
|
|
364
|
-
onlyIncludeActiveItemsIOS,
|
|
365
|
-
}: {
|
|
366
|
-
/** @deprecated Use alsoPublishToEventListenerIOS instead */
|
|
367
|
-
alsoPublishToEventListener?: boolean;
|
|
368
|
-
/** @deprecated Use onlyIncludeActiveItemsIOS instead */
|
|
369
|
-
onlyIncludeActiveItems?: boolean;
|
|
370
|
-
alsoPublishToEventListenerIOS?: boolean;
|
|
371
|
-
onlyIncludeActiveItemsIOS?: boolean;
|
|
372
|
-
} = {}): Promise<Purchase[]> =>
|
|
373
|
-
(
|
|
374
|
-
Platform.select({
|
|
375
|
-
ios: async () => {
|
|
376
|
-
return ExpoIapModule.getAvailableItems(
|
|
377
|
-
alsoPublishToEventListenerIOS ?? alsoPublishToEventListener,
|
|
378
|
-
onlyIncludeActiveItemsIOS ?? onlyIncludeActiveItems,
|
|
379
|
-
);
|
|
380
|
-
},
|
|
381
|
-
android: async () => {
|
|
382
|
-
// getPurchaseHistoryByType was removed in Google Play Billing Library v8
|
|
383
|
-
// Android doesn't provide purchase history anymore, only active purchases
|
|
384
|
-
console.warn(
|
|
385
|
-
'getPurchaseHistories is not supported on Android with Google Play Billing Library v8. Use getAvailablePurchases instead to get active purchases.',
|
|
386
|
-
);
|
|
387
|
-
return [];
|
|
388
|
-
},
|
|
389
|
-
}) || (() => Promise.resolve([]))
|
|
390
|
-
)();
|
|
369
|
+
// NOTE: `getPurchaseHistories` removed in v2.9.0. Use `getAvailablePurchases` instead.
|
|
391
370
|
|
|
392
371
|
export const getAvailablePurchases = ({
|
|
393
372
|
alsoPublishToEventListener = false,
|
|
@@ -652,15 +631,12 @@ export const finishTransaction = ({
|
|
|
652
631
|
|
|
653
632
|
if (!token) {
|
|
654
633
|
return Promise.reject(
|
|
655
|
-
new PurchaseError(
|
|
656
|
-
|
|
657
|
-
'
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
androidPurchase.productId,
|
|
662
|
-
'android',
|
|
663
|
-
),
|
|
634
|
+
new PurchaseError({
|
|
635
|
+
message: 'Purchase token is required to finish transaction',
|
|
636
|
+
code: 'E_DEVELOPER_ERROR' as ErrorCode,
|
|
637
|
+
productId: androidPurchase.productId,
|
|
638
|
+
platform: 'android',
|
|
639
|
+
}),
|
|
664
640
|
);
|
|
665
641
|
}
|
|
666
642
|
|
package/src/modules/ios.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
Product,
|
|
10
10
|
Purchase,
|
|
11
11
|
PurchaseError,
|
|
12
|
-
|
|
12
|
+
SubscriptionStatusIOS,
|
|
13
13
|
AppTransactionIOS,
|
|
14
14
|
} from '../ExpoIap.types';
|
|
15
15
|
import {Linking} from 'react-native';
|
|
@@ -122,7 +122,7 @@ export const isEligibleForIntroOfferIOS = (
|
|
|
122
122
|
*/
|
|
123
123
|
export const subscriptionStatusIOS = (
|
|
124
124
|
sku: string,
|
|
125
|
-
): Promise<
|
|
125
|
+
): Promise<SubscriptionStatusIOS[]> => {
|
|
126
126
|
return ExpoIapModule.subscriptionStatusIOS(sku);
|
|
127
127
|
};
|
|
128
128
|
|
|
@@ -310,12 +310,7 @@ export const requestPurchaseOnPromotedProductIOS = (): Promise<void> => {
|
|
|
310
310
|
return ExpoIapModule.requestPurchaseOnPromotedProductIOS();
|
|
311
311
|
};
|
|
312
312
|
|
|
313
|
-
|
|
314
|
-
* @deprecated Use requestPurchaseOnPromotedProductIOS instead. Will be removed in v2.9.0
|
|
315
|
-
*/
|
|
316
|
-
export const buyPromotedProductIOS = (): Promise<void> => {
|
|
317
|
-
return requestPurchaseOnPromotedProductIOS();
|
|
318
|
-
};
|
|
313
|
+
// NOTE: buyPromotedProductIOS removed in v2.9.0. Use requestPurchaseOnPromotedProductIOS.
|
|
319
314
|
|
|
320
315
|
/**
|
|
321
316
|
* Get pending transactions that haven't been finished yet (iOS only).
|
|
@@ -374,7 +369,7 @@ export const isEligibleForIntroOffer = (groupId: string): Promise<boolean> => {
|
|
|
374
369
|
*/
|
|
375
370
|
export const subscriptionStatus = (
|
|
376
371
|
sku: string,
|
|
377
|
-
): Promise<
|
|
372
|
+
): Promise<SubscriptionStatusIOS[]> => {
|
|
378
373
|
console.warn(
|
|
379
374
|
'`subscriptionStatus` is deprecated. Use `subscriptionStatusIOS` instead. This function will be removed in version 3.0.0.',
|
|
380
375
|
);
|
|
@@ -33,18 +33,6 @@ export type ProductAndroid = ProductCommon & {
|
|
|
33
33
|
oneTimePurchaseOfferDetailsAndroid?: ProductAndroidOneTimePurchaseOfferDetail;
|
|
34
34
|
platform: 'android';
|
|
35
35
|
subscriptionOfferDetailsAndroid?: ProductSubscriptionAndroidOfferDetail[];
|
|
36
|
-
/**
|
|
37
|
-
* @deprecated Use `nameAndroid` instead. This field will be removed in v2.9.0.
|
|
38
|
-
*/
|
|
39
|
-
name?: string;
|
|
40
|
-
/**
|
|
41
|
-
* @deprecated Use `oneTimePurchaseOfferDetailsAndroid` instead. This field will be removed in v2.9.0.
|
|
42
|
-
*/
|
|
43
|
-
oneTimePurchaseOfferDetails?: ProductAndroidOneTimePurchaseOfferDetail;
|
|
44
|
-
/**
|
|
45
|
-
* @deprecated Use `subscriptionOfferDetailsAndroid` instead. This field will be removed in v2.9.0.
|
|
46
|
-
*/
|
|
47
|
-
subscriptionOfferDetails?: ProductSubscriptionAndroidOfferDetail[];
|
|
48
36
|
};
|
|
49
37
|
|
|
50
38
|
type ProductSubscriptionAndroidOfferDetails = {
|
|
@@ -57,10 +45,6 @@ type ProductSubscriptionAndroidOfferDetails = {
|
|
|
57
45
|
|
|
58
46
|
export type ProductSubscriptionAndroid = ProductAndroid & {
|
|
59
47
|
subscriptionOfferDetailsAndroid: ProductSubscriptionAndroidOfferDetails[];
|
|
60
|
-
/**
|
|
61
|
-
* @deprecated Use `subscriptionOfferDetailsAndroid` instead. This field will be removed in v2.9.0.
|
|
62
|
-
*/
|
|
63
|
-
subscriptionOfferDetails?: ProductSubscriptionAndroidOfferDetails[];
|
|
64
48
|
};
|
|
65
49
|
|
|
66
50
|
// Legacy naming for backward compatibility
|
|
@@ -137,10 +121,7 @@ export enum PurchaseAndroidState {
|
|
|
137
121
|
}
|
|
138
122
|
|
|
139
123
|
// Legacy naming for backward compatibility
|
|
140
|
-
|
|
141
|
-
* @deprecated Use `PurchaseAndroidState` instead. This enum will be removed in v2.9.0.
|
|
142
|
-
*/
|
|
143
|
-
export const PurchaseStateAndroid = PurchaseAndroidState;
|
|
124
|
+
// Removed legacy alias `PurchaseStateAndroid` in v2.9.0
|
|
144
125
|
|
|
145
126
|
// Legacy naming for backward compatibility
|
|
146
127
|
export type ProductPurchaseAndroid = PurchaseCommon & {
|
|
@@ -163,19 +144,4 @@ export type ProductPurchaseAndroid = PurchaseCommon & {
|
|
|
163
144
|
// Preferred naming
|
|
164
145
|
export type PurchaseAndroid = ProductPurchaseAndroid;
|
|
165
146
|
|
|
166
|
-
//
|
|
167
|
-
/**
|
|
168
|
-
* @deprecated Use `ProductAndroidOneTimePurchaseOfferDetail` instead. This type will be removed in v2.9.0.
|
|
169
|
-
*/
|
|
170
|
-
export type OneTimePurchaseOfferDetails =
|
|
171
|
-
ProductAndroidOneTimePurchaseOfferDetail;
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* @deprecated Use `ProductSubscriptionAndroidOfferDetail` instead. This type will be removed in v2.9.0.
|
|
175
|
-
*/
|
|
176
|
-
export type SubscriptionOfferDetail = ProductSubscriptionAndroidOfferDetail;
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* @deprecated Use `ProductSubscriptionAndroidOfferDetails` instead. This type will be removed in v2.9.0.
|
|
180
|
-
*/
|
|
181
|
-
export type SubscriptionOfferAndroid = ProductSubscriptionAndroidOfferDetails;
|
|
147
|
+
// Removed legacy Android alias types in v2.9.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,
|
|
@@ -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 ({
|
|
@@ -420,9 +420,32 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
420
420
|
},
|
|
421
421
|
);
|
|
422
422
|
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
423
|
+
// Register purchase error listener EARLY. Ignore init-related errors until connected.
|
|
424
|
+
subscriptionsRef.current.purchaseError = purchaseErrorListener(
|
|
425
|
+
(error: PurchaseError) => {
|
|
426
|
+
if (
|
|
427
|
+
!connectedRef.current &&
|
|
428
|
+
error.code === ErrorCode.E_INIT_CONNECTION
|
|
429
|
+
) {
|
|
430
|
+
return; // Ignore initialization error before connected
|
|
431
|
+
}
|
|
432
|
+
const friendly = getUserFriendlyErrorMessage(error);
|
|
433
|
+
console.log('[useIAP] Purchase error callback triggered:', error);
|
|
434
|
+
if (isUserCancelledError(error)) {
|
|
435
|
+
console.log('[useIAP] User cancelled purchase');
|
|
436
|
+
} else if (isRecoverableError(error)) {
|
|
437
|
+
console.log('[useIAP] Recoverable purchase error:', friendly);
|
|
438
|
+
} else {
|
|
439
|
+
console.warn('[useIAP] Purchase error:', friendly);
|
|
440
|
+
}
|
|
441
|
+
setCurrentPurchase(undefined);
|
|
442
|
+
setCurrentPurchaseError(error);
|
|
443
|
+
|
|
444
|
+
if (optionsRef.current?.onPurchaseError) {
|
|
445
|
+
optionsRef.current.onPurchaseError(error);
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
);
|
|
426
449
|
|
|
427
450
|
if (Platform.OS === 'ios') {
|
|
428
451
|
// iOS promoted products listener
|
|
@@ -453,22 +476,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
453
476
|
subscriptionsRef.current.promotedProductsIOS?.remove();
|
|
454
477
|
subscriptionsRef.current.purchaseUpdate = undefined;
|
|
455
478
|
subscriptionsRef.current.promotedProductsIOS = undefined;
|
|
456
|
-
//
|
|
479
|
+
// Keep purchaseError listener registered to capture subsequent retries
|
|
457
480
|
return;
|
|
458
481
|
}
|
|
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
482
|
}, [refreshSubscriptionStatus]);
|
|
473
483
|
|
|
474
484
|
useEffect(() => {
|
|
@@ -491,7 +501,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
491
501
|
promotedProductsIOS,
|
|
492
502
|
promotedProductIdIOS,
|
|
493
503
|
subscriptions,
|
|
494
|
-
purchaseHistories,
|
|
495
504
|
finishTransaction,
|
|
496
505
|
availablePurchases,
|
|
497
506
|
currentPurchase,
|
|
@@ -501,7 +510,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
501
510
|
clearCurrentPurchase,
|
|
502
511
|
clearCurrentPurchaseError,
|
|
503
512
|
getAvailablePurchases: getAvailablePurchasesInternal,
|
|
504
|
-
getPurchaseHistories: getPurchaseHistoriesInternal,
|
|
505
513
|
fetchProducts: fetchProductsInternal,
|
|
506
514
|
requestProducts: requestProductsInternal,
|
|
507
515
|
requestPurchase: requestPurchaseWithReset,
|
|
@@ -511,7 +519,6 @@ export function useIAP(options?: UseIAPOptions): UseIap {
|
|
|
511
519
|
getSubscriptions: getSubscriptionsInternal,
|
|
512
520
|
getPromotedProductIOS,
|
|
513
521
|
requestPurchaseOnPromotedProductIOS,
|
|
514
|
-
buyPromotedProductIOS: requestPurchaseOnPromotedProductIOS, // deprecated alias
|
|
515
522
|
getActiveSubscriptions: getActiveSubscriptionsInternal,
|
|
516
523
|
hasActiveSubscriptions: hasActiveSubscriptionsInternal,
|
|
517
524
|
};
|
|
@@ -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
|
}
|