expo-iap 3.1.3 → 3.1.5

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 (41) hide show
  1. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +5 -5
  2. package/build/index.d.ts +1 -1
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +3 -7
  5. package/build/index.js.map +1 -1
  6. package/build/modules/android.d.ts.map +1 -1
  7. package/build/modules/android.js +3 -2
  8. package/build/modules/android.js.map +1 -1
  9. package/build/modules/ios.d.ts +0 -15
  10. package/build/modules/ios.d.ts.map +1 -1
  11. package/build/modules/ios.js +1 -22
  12. package/build/modules/ios.js.map +1 -1
  13. package/build/types.d.ts +7 -1
  14. package/build/types.d.ts.map +1 -1
  15. package/build/types.js.map +1 -1
  16. package/build/useIAP.d.ts +0 -2
  17. package/build/useIAP.d.ts.map +1 -1
  18. package/build/useIAP.js +31 -5
  19. package/build/useIAP.js.map +1 -1
  20. package/coverage/clover.xml +236 -242
  21. package/coverage/coverage-final.json +3 -3
  22. package/coverage/lcov-report/index.html +22 -22
  23. package/coverage/lcov-report/src/helpers/index.html +1 -1
  24. package/coverage/lcov-report/src/helpers/subscription.ts.html +1 -1
  25. package/coverage/lcov-report/src/index.html +15 -15
  26. package/coverage/lcov-report/src/index.ts.html +13 -28
  27. package/coverage/lcov-report/src/modules/android.ts.html +36 -6
  28. package/coverage/lcov-report/src/modules/index.html +12 -12
  29. package/coverage/lcov-report/src/modules/ios.ts.html +7 -73
  30. package/coverage/lcov-report/src/utils/errorMapping.ts.html +1 -1
  31. package/coverage/lcov-report/src/utils/index.html +1 -1
  32. package/coverage/lcov.info +414 -422
  33. package/ios/ExpoIapHelper.swift +2 -4
  34. package/ios/ExpoIapModule.swift +3 -3
  35. package/openiap-versions.json +3 -3
  36. package/package.json +1 -1
  37. package/src/index.ts +3 -8
  38. package/src/modules/android.ts +12 -2
  39. package/src/modules/ios.ts +1 -23
  40. package/src/types.ts +7 -1
  41. package/src/useIAP.ts +45 -10
@@ -133,10 +133,8 @@ enum ExpoIapHelper {
133
133
  }
134
134
 
135
135
  static func cleanupListeners() {
136
- // Cancel and clear subscriptions to prevent memory leaks
137
- listeners.forEach { sub in
138
- sub.cancel()
139
- }
136
+ // Clear subscriptions to prevent memory leaks
137
+ // Subscription deinit will automatically call onRemove closure
140
138
  listeners.removeAll()
141
139
  }
142
140
 
@@ -246,11 +246,11 @@ public final class ExpoIapModule: Module {
246
246
  return nil
247
247
  }
248
248
 
249
- AsyncFunction("getStorefrontIOS") { () async throws -> String in
250
- ExpoIapLog.payload("getStorefrontIOS", payload: nil)
249
+ AsyncFunction("getStorefront") { () async throws -> String in
250
+ ExpoIapLog.payload("getStorefront", payload: nil)
251
251
  try await ExpoIapHelper.ensureConnection(isInitialized: self.isInitialized)
252
252
  let storefront = try await OpenIapModule.shared.getStorefrontIOS()
253
- ExpoIapLog.result("getStorefrontIOS", value: storefront)
253
+ ExpoIapLog.result("getStorefront", value: storefront)
254
254
  return storefront
255
255
  }
256
256
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "apple": "1.2.3",
3
- "google": "1.2.7",
4
- "gql": "1.0.8"
2
+ "apple": "1.2.4",
3
+ "google": "1.2.8",
4
+ "gql": "1.0.9"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "3.1.3",
3
+ "version": "3.1.5",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
package/src/index.ts CHANGED
@@ -9,7 +9,6 @@ import {
9
9
  validateReceiptIOS,
10
10
  deepLinkToSubscriptionsIOS,
11
11
  syncIOS,
12
- getStorefrontIOS,
13
12
  } from './modules/ios';
14
13
  import {
15
14
  isProductAndroid,
@@ -292,15 +291,11 @@ export const getAvailablePurchases: QueryField<
292
291
  return normalizePurchaseArray(purchases as Purchase[]);
293
292
  };
294
293
 
295
- export const getStorefront: QueryField<'getStorefrontIOS'> = async () => {
296
- // Cross-platform storefront
297
- if (Platform.OS === 'android') {
298
- if (typeof ExpoIapModule.getStorefrontAndroid === 'function') {
299
- return ExpoIapModule.getStorefrontAndroid();
300
- }
294
+ export const getStorefront: QueryField<'getStorefront'> = async () => {
295
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
301
296
  return '';
302
297
  }
303
- return getStorefrontIOS();
298
+ return ExpoIapModule.getStorefront();
304
299
  };
305
300
 
306
301
  /**
@@ -11,6 +11,16 @@ import type {
11
11
  ReceiptValidationResultAndroid,
12
12
  } from '../types';
13
13
 
14
+ type NativeAndroidModule = {
15
+ deepLinkToSubscriptionsAndroid?: (params: {
16
+ skuAndroid?: string;
17
+ packageNameAndroid?: string;
18
+ }) => Promise<void> | void;
19
+ getStorefront?: () => Promise<string> | string;
20
+ };
21
+
22
+ const nativeAndroidModule = ExpoIapModule as NativeAndroidModule;
23
+
14
24
  // Type guards
15
25
  export function isProductAndroid<T extends {platform?: string}>(
16
26
  item: unknown,
@@ -46,8 +56,8 @@ export const deepLinkToSubscriptionsAndroid = async (
46
56
  const packageName = options?.packageNameAndroid ?? undefined;
47
57
 
48
58
  // Prefer native deep link implementation via OpenIAP module
49
- if (ExpoIapModule?.deepLinkToSubscriptionsAndroid) {
50
- return (ExpoIapModule as any).deepLinkToSubscriptionsAndroid({
59
+ if (nativeAndroidModule?.deepLinkToSubscriptionsAndroid) {
60
+ return nativeAndroidModule.deepLinkToSubscriptionsAndroid({
51
61
  skuAndroid: sku,
52
62
  packageNameAndroid: packageName,
53
63
  });
@@ -16,7 +16,7 @@ import type {
16
16
  SubscriptionStatusIOS,
17
17
  } from '../types';
18
18
  import type {PurchaseError} from '../utils/errorMapping';
19
- import {Linking, Platform} from 'react-native';
19
+ import {Linking} from 'react-native';
20
20
 
21
21
  export type TransactionEvent = {
22
22
  transaction?: Purchase;
@@ -178,28 +178,6 @@ export const getReceiptDataIOS: QueryField<'getReceiptDataIOS'> = async () => {
178
178
 
179
179
  export const getReceiptIOS = getReceiptDataIOS;
180
180
 
181
- /**
182
- * Retrieves the current storefront information from the iOS App Store.
183
- *
184
- * @returns Promise resolving to the storefront country code
185
- * @throws Error if called on non-iOS platform
186
- *
187
- * @example
188
- * ```typescript
189
- * const storefront = await getStorefrontIOS();
190
- * console.log(storefront); // 'US'
191
- * ```
192
- *
193
- * @platform iOS
194
- */
195
- export const getStorefrontIOS: QueryField<'getStorefrontIOS'> = async () => {
196
- if (Platform.OS !== 'ios') {
197
- console.warn('getStorefrontIOS: This method is only available on iOS');
198
- return '';
199
- }
200
- return ExpoIapModule.getStorefrontIOS();
201
- };
202
-
203
181
  /**
204
182
  * Check if a transaction is verified through StoreKit 2.
205
183
  * StoreKit 2 performs local verification of transaction JWS signatures.
package/src/types.ts CHANGED
@@ -449,7 +449,12 @@ export interface Query {
449
449
  getPromotedProductIOS?: Promise<(ProductIOS | null)>;
450
450
  /** Get base64-encoded receipt data for validation */
451
451
  getReceiptDataIOS?: Promise<(string | null)>;
452
- /** Get the current App Store storefront country code */
452
+ /** Get the current storefront country code */
453
+ getStorefront: Promise<string>;
454
+ /**
455
+ * Get the current App Store storefront country code
456
+ * @deprecated Use getStorefront
457
+ */
453
458
  getStorefrontIOS: Promise<string>;
454
459
  /** Get the transaction JWS (StoreKit 2) */
455
460
  getTransactionJwsIOS?: Promise<(string | null)>;
@@ -680,6 +685,7 @@ export type QueryArgsMap = {
680
685
  getPendingTransactionsIOS: never;
681
686
  getPromotedProductIOS: never;
682
687
  getReceiptDataIOS: never;
688
+ getStorefront: never;
683
689
  getStorefrontIOS: never;
684
690
  getTransactionJwsIOS: QueryGetTransactionJwsIosArgs;
685
691
  hasActiveSubscriptions: QueryHasActiveSubscriptionsArgs;
package/src/useIAP.ts CHANGED
@@ -19,11 +19,11 @@ import {
19
19
  hasActiveSubscriptions,
20
20
  type ActiveSubscription,
21
21
  type ProductTypeInput,
22
- restorePurchases,
23
22
  } from './index';
24
23
  import {
25
24
  getPromotedProductIOS,
26
25
  requestPurchaseOnPromotedProductIOS,
26
+ syncIOS,
27
27
  } from './modules/ios';
28
28
 
29
29
  // Types
@@ -37,6 +37,8 @@ import type {
37
37
  PurchaseInput,
38
38
  ReceiptValidationProps,
39
39
  ReceiptValidationResult,
40
+ ProductAndroid,
41
+ ProductSubscriptionIOS,
40
42
  } from './types';
41
43
  import {ErrorCode} from './types';
42
44
  import type {PurchaseError} from './utils/errorMapping';
@@ -82,8 +84,6 @@ type UseIap = {
82
84
  export interface UseIAPOptions {
83
85
  onPurchaseSuccess?: (purchase: Purchase) => void;
84
86
  onPurchaseError?: (error: PurchaseError) => void;
85
- onSyncError?: (error: Error) => void;
86
- shouldAutoSyncPurchases?: boolean; // New option to control auto-syncing
87
87
  onPromotedProductIOS?: (product: Product) => void;
88
88
  }
89
89
 
@@ -161,6 +161,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
161
161
  if (!value) {
162
162
  return 'in-app';
163
163
  }
164
+
164
165
  const normalized = value.trim().toLowerCase().replace(/[_-]/g, '');
165
166
  return normalized === 'subs' ? 'subs' : 'in-app';
166
167
  },
@@ -193,6 +194,8 @@ export function useIAP(options?: UseIAPOptions): UseIap {
193
194
  const result = await fetchProducts(request);
194
195
  const items = (result ?? []) as (Product | ProductSubscription)[];
195
196
 
197
+ console.log('Fetched products:', items);
198
+
196
199
  if (queryType === 'subs') {
197
200
  const subscriptionsResult = items as ProductSubscription[];
198
201
  setSubscriptions((prevSubscriptions) =>
@@ -212,12 +215,40 @@ export function useIAP(options?: UseIAPOptions): UseIap {
212
215
  ),
213
216
  );
214
217
  } else {
215
- const productItems = items.filter(
216
- (item) => canonicalProductType(item.type as string) === 'in-app',
217
- ) as Product[];
218
- const subscriptionItems = items.filter(
219
- (item) => canonicalProductType(item.type as string) === 'subs',
220
- ) as ProductSubscription[];
218
+ // For 'all' type, need to properly distinguish between products and subscriptions
219
+ // On Android, check subscriptionOfferDetailsAndroid to determine if it's a real subscription
220
+ const productItems = items.filter((item) => {
221
+ // iOS: check type
222
+ if (Platform.OS === 'ios') {
223
+ return canonicalProductType(item.type as string) === 'in-app';
224
+ }
225
+ // Android: check if it has actual subscription details
226
+ const androidItem = item as ProductAndroid;
227
+ return (
228
+ !androidItem.subscriptionOfferDetailsAndroid ||
229
+ (Array.isArray(androidItem.subscriptionOfferDetailsAndroid) &&
230
+ androidItem.subscriptionOfferDetailsAndroid.length === 0)
231
+ );
232
+ }) as Product[];
233
+
234
+ const subscriptionItems = items.filter((item) => {
235
+ // iOS: check type
236
+ if (Platform.OS === 'ios') {
237
+ return (
238
+ canonicalProductType(
239
+ item.type as ProductSubscriptionIOS['type'],
240
+ ) === 'subs'
241
+ );
242
+ }
243
+ // Android: check if it has actual subscription details
244
+ const androidItem = item as ProductAndroid;
245
+
246
+ return (
247
+ androidItem.subscriptionOfferDetailsAndroid &&
248
+ Array.isArray(androidItem.subscriptionOfferDetailsAndroid) &&
249
+ androidItem.subscriptionOfferDetailsAndroid.length > 0
250
+ );
251
+ }) as ProductSubscription[];
221
252
 
222
253
  setProducts((prevProducts) =>
223
254
  mergeWithDuplicateCheck(
@@ -321,7 +352,11 @@ export function useIAP(options?: UseIAPOptions): UseIap {
321
352
  // Android: fetch available purchases directly.
322
353
  const restorePurchasesInternal = useCallback(async (): Promise<void> => {
323
354
  try {
324
- await restorePurchases();
355
+ // iOS: Try to sync first, but don't fail if sync errors occur
356
+ if (Platform.OS === 'ios') {
357
+ await syncIOS().catch(() => undefined); // syncIOS returns Promise<boolean>, we don't need the result
358
+ }
359
+
325
360
  const purchases = await getAvailablePurchases({
326
361
  alsoPublishToEventListenerIOS: false,
327
362
  onlyIncludeActiveItemsIOS: true,