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
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -197,7 +197,7 @@ public class ExpoIapModule: Module {
|
|
|
197
197
|
logDebug("Purchase request completed successfully")
|
|
198
198
|
} catch {
|
|
199
199
|
logDebug("Purchase request failed with error: \(error)")
|
|
200
|
-
throw error
|
|
200
|
+
throw OpenIapError.storeKitError(error: error)
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
@@ -263,19 +263,21 @@ public class ExpoIapModule: Module {
|
|
|
263
263
|
|
|
264
264
|
AsyncFunction("validateReceiptIOS") { (sku: String) async throws -> [String: Any?] in
|
|
265
265
|
logDebug("validateReceiptIOS called for sku: \(sku)")
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
266
|
+
do {
|
|
267
|
+
// Use OpenIapReceiptValidationProps to keep naming parity with OpenIAP
|
|
268
|
+
let props = OpenIapReceiptValidationProps(sku: sku)
|
|
269
|
+
let result = try await OpenIapModule.shared.validateReceiptIOS(props)
|
|
270
|
+
return [
|
|
271
|
+
"isValid": result.isValid,
|
|
272
|
+
"receiptData": result.receiptData,
|
|
273
|
+
"jwsRepresentation": result.jwsRepresentation,
|
|
274
|
+
// Populate unified purchaseToken for iOS as alias of JWS
|
|
275
|
+
"purchaseToken": result.jwsRepresentation,
|
|
276
|
+
"latestTransaction": result.latestTransaction.map { OpenIapSerialization.purchase($0) },
|
|
277
|
+
]
|
|
278
|
+
} catch {
|
|
279
|
+
throw OpenIapError.invalidReceipt
|
|
280
|
+
}
|
|
279
281
|
}
|
|
280
282
|
|
|
281
283
|
// MARK: - iOS Specific Features
|
|
@@ -312,26 +314,15 @@ public class ExpoIapModule: Module {
|
|
|
312
314
|
AsyncFunction("getPromotedProductIOS") { () async throws -> [String: Any?]? in
|
|
313
315
|
logDebug("getPromotedProductIOS called")
|
|
314
316
|
|
|
315
|
-
if let
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
"priceLocale": [
|
|
322
|
-
"currencyCode": promotedProduct.priceLocale.currencyCode,
|
|
323
|
-
"currencySymbol": promotedProduct.priceLocale.currencySymbol
|
|
324
|
-
]
|
|
325
|
-
]
|
|
317
|
+
if let promoted = try await OpenIapModule.shared.getPromotedProductIOS() {
|
|
318
|
+
// Fetch full product info by SKU to conform to OpenIapProduct
|
|
319
|
+
let request = OpenIapProductRequest(skus: [promoted.productIdentifier], type: .all)
|
|
320
|
+
let products = try await OpenIapModule.shared.fetchProducts(request)
|
|
321
|
+
let serialized = OpenIapSerialization.products(products)
|
|
322
|
+
return serialized.first
|
|
326
323
|
}
|
|
327
324
|
return nil
|
|
328
325
|
}
|
|
329
|
-
|
|
330
|
-
AsyncFunction("buyPromotedProductIOS") { () async throws in
|
|
331
|
-
logDebug("buyPromotedProductIOS called")
|
|
332
|
-
try await OpenIapModule.shared.requestPurchaseOnPromotedProductIOS()
|
|
333
|
-
}
|
|
334
|
-
|
|
335
326
|
AsyncFunction("getStorefrontIOS") { () async throws -> String in
|
|
336
327
|
logDebug("getStorefrontIOS called")
|
|
337
328
|
return try await OpenIapModule.shared.getStorefrontIOS()
|
|
@@ -363,15 +354,41 @@ public class ExpoIapModule: Module {
|
|
|
363
354
|
logDebug("subscriptionStatusIOS called for sku: \(sku)")
|
|
364
355
|
|
|
365
356
|
if let statuses = try await OpenIapModule.shared.subscriptionStatusIOS(sku: sku) {
|
|
357
|
+
// Align output with SubscriptionStatusIOS in TS:
|
|
358
|
+
// { state: SubscriptionState; renewalInfo?: { jsonRepresentation?: string; willAutoRenew: boolean; autoRenewPreference?: string } }
|
|
366
359
|
return statuses.map { status in
|
|
367
|
-
|
|
368
|
-
"state": status.state
|
|
369
|
-
"autoRenewStatus": status.renewalInfo?.autoRenewStatus,
|
|
370
|
-
"autoRenewPreference": status.renewalInfo?.autoRenewPreference,
|
|
371
|
-
"expirationReason": status.renewalInfo?.expirationReason,
|
|
372
|
-
"currentProductID": status.renewalInfo?.currentProductID,
|
|
373
|
-
"gracePeriodExpirationDate": status.renewalInfo?.gracePeriodExpirationDate
|
|
360
|
+
var dict: [String: Any?] = [
|
|
361
|
+
"state": status.state
|
|
374
362
|
]
|
|
363
|
+
|
|
364
|
+
if let info = status.renewalInfo {
|
|
365
|
+
// Convert autoRenewStatus to a proper boolean for willAutoRenew
|
|
366
|
+
let willAutoRenew: Bool = {
|
|
367
|
+
// Try boolean first
|
|
368
|
+
if let b = info.autoRenewStatus as? Bool { return b }
|
|
369
|
+
// Fallback to string normalization
|
|
370
|
+
let normalized = String(describing: info.autoRenewStatus).lowercased()
|
|
371
|
+
let truthy = Set([
|
|
372
|
+
"willrenew",
|
|
373
|
+
"will_autorenew",
|
|
374
|
+
"will-auto-renew",
|
|
375
|
+
"auto_renew_on",
|
|
376
|
+
"true",
|
|
377
|
+
"1",
|
|
378
|
+
"on",
|
|
379
|
+
"yes",
|
|
380
|
+
])
|
|
381
|
+
return truthy.contains(normalized)
|
|
382
|
+
}()
|
|
383
|
+
|
|
384
|
+
let renewalInfo: [String: Any?] = [
|
|
385
|
+
"willAutoRenew": willAutoRenew,
|
|
386
|
+
"autoRenewPreference": info.autoRenewPreference
|
|
387
|
+
]
|
|
388
|
+
dict["renewalInfo"] = renewalInfo
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return dict
|
|
375
392
|
}
|
|
376
393
|
}
|
|
377
394
|
return nil
|
|
@@ -379,20 +396,26 @@ public class ExpoIapModule: Module {
|
|
|
379
396
|
|
|
380
397
|
AsyncFunction("currentEntitlementIOS") { (sku: String) async throws -> [String: Any?]? in
|
|
381
398
|
logDebug("currentEntitlementIOS called for sku: \(sku)")
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
399
|
+
do {
|
|
400
|
+
if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku) {
|
|
401
|
+
return OpenIapSerialization.purchase(entitlement)
|
|
402
|
+
}
|
|
403
|
+
return nil
|
|
404
|
+
} catch {
|
|
405
|
+
throw OpenIapError.productNotFound(id: sku)
|
|
385
406
|
}
|
|
386
|
-
return nil
|
|
387
407
|
}
|
|
388
408
|
|
|
389
409
|
AsyncFunction("latestTransactionIOS") { (sku: String) async throws -> [String: Any?]? in
|
|
390
410
|
logDebug("latestTransactionIOS called for sku: \(sku)")
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
411
|
+
do {
|
|
412
|
+
if let transaction = try await OpenIapModule.shared.latestTransactionIOS(sku: sku) {
|
|
413
|
+
return OpenIapSerialization.purchase(transaction)
|
|
414
|
+
}
|
|
415
|
+
return nil
|
|
416
|
+
} catch {
|
|
417
|
+
throw OpenIapError.productNotFound(id: sku)
|
|
394
418
|
}
|
|
395
|
-
return nil
|
|
396
419
|
}
|
|
397
420
|
}
|
|
398
421
|
|
|
@@ -411,16 +434,14 @@ public class ExpoIapModule: Module {
|
|
|
411
434
|
}
|
|
412
435
|
}
|
|
413
436
|
|
|
414
|
-
purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self]
|
|
437
|
+
purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] (event: OpenIapErrorEvent) in
|
|
415
438
|
Task { @MainActor in
|
|
416
439
|
guard let self else { return }
|
|
417
440
|
logDebug("❌ Purchase error callback - sending error event")
|
|
418
|
-
// Use OpenIapPurchaseError alias for clarity/parity
|
|
419
|
-
let err: OpenIapPurchaseError = error
|
|
420
441
|
let errorData: [String: Any?] = [
|
|
421
|
-
"code":
|
|
422
|
-
"message":
|
|
423
|
-
"productId":
|
|
442
|
+
"code": event.code,
|
|
443
|
+
"message": event.message,
|
|
444
|
+
"productId": event.productId
|
|
424
445
|
]
|
|
425
446
|
self.sendEvent(OpenIapEvent.PurchaseError, errorData)
|
|
426
447
|
}
|
package/package.json
CHANGED
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
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isProductIOS,
|
|
9
9
|
validateReceiptIOS,
|
|
10
10
|
deepLinkToSubscriptionsIOS,
|
|
11
|
+
syncIOS,
|
|
11
12
|
} from './modules/ios';
|
|
12
13
|
import {
|
|
13
14
|
isProductAndroid,
|
|
@@ -143,7 +144,12 @@ export const getProducts = async (skus: string[]): Promise<Product[]> => {
|
|
|
143
144
|
"`getProducts` is deprecated. Use `fetchProducts({ skus, type: 'inapp' })` instead. This function will be removed in version 3.0.0.",
|
|
144
145
|
);
|
|
145
146
|
if (!skus?.length) {
|
|
146
|
-
return Promise.reject(
|
|
147
|
+
return Promise.reject(
|
|
148
|
+
new PurchaseError({
|
|
149
|
+
message: 'No SKUs provided',
|
|
150
|
+
code: ErrorCode.E_EMPTY_SKU_LIST,
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
return Platform.select({
|
|
@@ -177,7 +183,12 @@ export const getSubscriptions = async (
|
|
|
177
183
|
"`getSubscriptions` is deprecated. Use `fetchProducts({ skus, type: 'subs' })` instead. This function will be removed in version 3.0.0.",
|
|
178
184
|
);
|
|
179
185
|
if (!skus?.length) {
|
|
180
|
-
return Promise.reject(
|
|
186
|
+
return Promise.reject(
|
|
187
|
+
new PurchaseError({
|
|
188
|
+
message: 'No SKUs provided',
|
|
189
|
+
code: ErrorCode.E_EMPTY_SKU_LIST,
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
181
192
|
}
|
|
182
193
|
|
|
183
194
|
return Platform.select({
|
|
@@ -245,7 +256,10 @@ export const fetchProducts = async ({
|
|
|
245
256
|
type?: 'inapp' | 'subs';
|
|
246
257
|
}): Promise<Product[] | SubscriptionProduct[]> => {
|
|
247
258
|
if (!skus?.length) {
|
|
248
|
-
throw new
|
|
259
|
+
throw new PurchaseError({
|
|
260
|
+
message: 'No SKUs provided',
|
|
261
|
+
code: ErrorCode.E_EMPTY_SKU_LIST,
|
|
262
|
+
});
|
|
249
263
|
}
|
|
250
264
|
|
|
251
265
|
if (Platform.OS === 'ios') {
|
|
@@ -344,7 +358,8 @@ export const getPurchaseHistory = ({
|
|
|
344
358
|
console.warn(
|
|
345
359
|
'`getPurchaseHistory` is deprecated. Use `getPurchaseHistories` instead. This function will be removed in version 3.0.0.',
|
|
346
360
|
);
|
|
347
|
-
|
|
361
|
+
// Use available purchases as a best-effort replacement
|
|
362
|
+
return getAvailablePurchases({
|
|
348
363
|
alsoPublishToEventListenerIOS:
|
|
349
364
|
alsoPublishToEventListenerIOS ?? alsoPublishToEventListener,
|
|
350
365
|
onlyIncludeActiveItemsIOS:
|
|
@@ -352,42 +367,7 @@ export const getPurchaseHistory = ({
|
|
|
352
367
|
});
|
|
353
368
|
};
|
|
354
369
|
|
|
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
|
-
)();
|
|
370
|
+
// NOTE: `getPurchaseHistories` removed in v2.9.0. Use `getAvailablePurchases` instead.
|
|
391
371
|
|
|
392
372
|
export const getAvailablePurchases = ({
|
|
393
373
|
alsoPublishToEventListener = false,
|
|
@@ -419,6 +399,40 @@ export const getAvailablePurchases = ({
|
|
|
419
399
|
}) || (() => Promise.resolve([]))
|
|
420
400
|
)();
|
|
421
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Restore completed transactions (cross-platform behavior)
|
|
404
|
+
*
|
|
405
|
+
* - iOS: perform a lightweight sync to refresh transactions and ignore sync errors,
|
|
406
|
+
* then fetch available purchases to surface restored items to the app.
|
|
407
|
+
* - Android: simply fetch available purchases (restoration happens via query).
|
|
408
|
+
*
|
|
409
|
+
* This helper returns the restored/available purchases so callers can update UI/state.
|
|
410
|
+
*
|
|
411
|
+
* @param options.alsoPublishToEventListenerIOS - iOS only: whether to also publish to the event listener
|
|
412
|
+
* @param options.onlyIncludeActiveItemsIOS - iOS only: whether to only include active items
|
|
413
|
+
* @returns Promise resolving to the list of available/restored purchases
|
|
414
|
+
*/
|
|
415
|
+
export const restorePurchases = async (
|
|
416
|
+
options: {
|
|
417
|
+
alsoPublishToEventListenerIOS?: boolean;
|
|
418
|
+
onlyIncludeActiveItemsIOS?: boolean;
|
|
419
|
+
} = {},
|
|
420
|
+
): Promise<Purchase[]> => {
|
|
421
|
+
if (Platform.OS === 'ios') {
|
|
422
|
+
// Perform best-effort sync on iOS and ignore sync errors to avoid blocking restore flow
|
|
423
|
+
await syncIOS().catch(() => undefined);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Then, fetch available purchases for both platforms
|
|
427
|
+
const purchases = await getAvailablePurchases({
|
|
428
|
+
alsoPublishToEventListenerIOS:
|
|
429
|
+
options.alsoPublishToEventListenerIOS ?? false,
|
|
430
|
+
onlyIncludeActiveItemsIOS: options.onlyIncludeActiveItemsIOS ?? true,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return purchases;
|
|
434
|
+
};
|
|
435
|
+
|
|
422
436
|
const offerToRecordIOS = (
|
|
423
437
|
offer: PaymentDiscount | undefined,
|
|
424
438
|
): Record<keyof PaymentDiscount, string> | undefined => {
|
|
@@ -652,15 +666,12 @@ export const finishTransaction = ({
|
|
|
652
666
|
|
|
653
667
|
if (!token) {
|
|
654
668
|
return Promise.reject(
|
|
655
|
-
new PurchaseError(
|
|
656
|
-
|
|
657
|
-
'
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
androidPurchase.productId,
|
|
662
|
-
'android',
|
|
663
|
-
),
|
|
669
|
+
new PurchaseError({
|
|
670
|
+
message: 'Purchase token is required to finish transaction',
|
|
671
|
+
code: 'E_DEVELOPER_ERROR' as ErrorCode,
|
|
672
|
+
productId: androidPurchase.productId,
|
|
673
|
+
platform: 'android',
|
|
674
|
+
}),
|
|
664
675
|
);
|
|
665
676
|
}
|
|
666
677
|
|
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
|