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.
Files changed (39) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/CONTRIBUTING.md +2 -2
  3. package/README.md +3 -3
  4. package/build/ExpoIap.types.d.ts +33 -15
  5. package/build/ExpoIap.types.d.ts.map +1 -1
  6. package/build/ExpoIap.types.js +64 -17
  7. package/build/ExpoIap.types.js.map +1 -1
  8. package/build/index.d.ts +15 -11
  9. package/build/index.d.ts.map +1 -1
  10. package/build/index.js +48 -23
  11. package/build/index.js.map +1 -1
  12. package/build/modules/ios.d.ts +3 -7
  13. package/build/modules/ios.d.ts.map +1 -1
  14. package/build/modules/ios.js +1 -6
  15. package/build/modules/ios.js.map +1 -1
  16. package/build/types/ExpoIapAndroid.types.d.ts +0 -32
  17. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
  18. package/build/types/ExpoIapAndroid.types.js +1 -5
  19. package/build/types/ExpoIapAndroid.types.js.map +1 -1
  20. package/build/types/ExpoIapIOS.types.d.ts +3 -27
  21. package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
  22. package/build/types/ExpoIapIOS.types.js.map +1 -1
  23. package/build/useIAP.d.ts +1 -5
  24. package/build/useIAP.d.ts.map +1 -1
  25. package/build/useIAP.js +47 -41
  26. package/build/useIAP.js.map +1 -1
  27. package/build/utils/errorMapping.d.ts.map +1 -1
  28. package/build/utils/errorMapping.js +24 -0
  29. package/build/utils/errorMapping.js.map +1 -1
  30. package/ios/ExpoIap.podspec +1 -1
  31. package/ios/ExpoIapModule.swift +73 -52
  32. package/package.json +1 -1
  33. package/src/ExpoIap.types.ts +84 -37
  34. package/src/index.ts +60 -49
  35. package/src/modules/ios.ts +4 -9
  36. package/src/types/ExpoIapAndroid.types.ts +2 -36
  37. package/src/types/ExpoIapIOS.types.ts +3 -27
  38. package/src/useIAP.ts +53 -48
  39. package/src/utils/errorMapping.ts +24 -0
@@ -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
- // Use OpenIapReceiptValidationProps to keep naming parity with OpenIAP
268
- let props = OpenIapReceiptValidationProps(sku: sku)
269
- let result = try await OpenIapModule.shared.validateReceiptIOS(props)
270
-
271
- return [
272
- "isValid": result.isValid,
273
- "receiptData": result.receiptData,
274
- "jwsRepresentation": result.jwsRepresentation,
275
- // Populate unified purchaseToken for iOS as alias of JWS
276
- "purchaseToken": result.jwsRepresentation,
277
- "latestTransaction": result.latestTransaction.map { OpenIapSerialization.purchase($0) },
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 promotedProduct = try await OpenIapModule.shared.getPromotedProductIOS() {
316
- return [
317
- "productIdentifier": promotedProduct.productIdentifier,
318
- "localizedTitle": promotedProduct.localizedTitle,
319
- "localizedDescription": promotedProduct.localizedDescription,
320
- "price": promotedProduct.price,
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
- return [
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
- if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku) {
384
- return OpenIapSerialization.purchase(entitlement)
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
- if let transaction = try await OpenIapModule.shared.latestTransactionIOS(sku: sku) {
393
- return OpenIapSerialization.purchase(transaction)
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] error in
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": err.code,
422
- "message": err.message,
423
- "productId": err.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "2.9.0",
3
+ "version": "2.9.2",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -70,15 +70,7 @@ export type Purchase =
70
70
  | (PurchaseAndroid & AndroidPlatform)
71
71
  | (PurchaseIOS & IosPlatform);
72
72
 
73
- // Legacy type aliases - deprecated, use Purchase instead
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
- constructor(
188
- public name: string,
189
- public message: string,
190
- public responseCode?: number,
191
- public debugMessage?: string,
192
- public code?: ErrorCode,
193
- public productId?: string,
194
- public platform?: 'ios' | 'android',
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
- this.message = message;
198
- this.responseCode = responseCode;
199
- this.debugMessage = debugMessage;
200
- this.code = code;
201
- this.productId = productId;
202
- this.platform = platform;
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
- '[expo-iap]: PurchaseError',
221
- errorData.message || 'Unknown error occurred',
222
- errorData.responseCode,
223
- errorData.debugMessage,
224
- errorCode,
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
- const mapping = ErrorCodeMapping[platform];
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
- return (
285
- ErrorCodeMapping[platform][errorCode] ??
286
- (platform === 'ios' ? 0 : 'E_UNKNOWN')
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(new Error('"skus" is required'));
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(new Error('"skus" is required'));
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 Error('No SKUs provided');
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
- return getPurchaseHistories({
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
- '[expo-iap]: PurchaseError',
657
- 'Purchase token is required to finish transaction',
658
- undefined,
659
- undefined,
660
- 'E_DEVELOPER_ERROR' as ErrorCode,
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
 
@@ -9,7 +9,7 @@ import type {
9
9
  Product,
10
10
  Purchase,
11
11
  PurchaseError,
12
- ProductStatusIOS,
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<ProductStatusIOS[]> => {
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<ProductStatusIOS[]> => {
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
- // Legacy type aliases for backward compatibility
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