expo-iap 2.7.11 → 2.7.13

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.
@@ -48,9 +48,10 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
48
48
  }
49
49
 
50
50
  var purchaseMap: [String: Any?] = [
51
- "id": transaction.productID,
51
+ "id": String(transaction.id),
52
+ "productId": transaction.productID,
52
53
  "ids": [transaction.productID],
53
- "transactionId": String(transaction.id),
54
+ "transactionId": String(transaction.id), // @deprecated - use id instead
54
55
  "transactionDate": transaction.purchaseDate.timeIntervalSince1970 * 1000,
55
56
  "transactionReceipt": jwsReceipt,
56
57
  "platform": "ios",
@@ -240,7 +241,7 @@ public class ExpoIapModule: Module {
240
241
  "ERROR_CODES": IapErrorCode.toDictionary()
241
242
  ])
242
243
 
243
- Events(IapEvent.PurchaseUpdated, IapEvent.PurchaseError)
244
+ Events(IapEvent.PurchaseUpdated, IapEvent.PurchaseError, IapEvent.PromotedProductIOS)
244
245
 
245
246
  OnStartObserving {
246
247
  self.hasListeners = true
@@ -493,6 +494,14 @@ public class ExpoIapModule: Module {
493
494
  options.insert(.appAccountToken(appAccountUUID))
494
495
  }
495
496
  guard let windowScene = await self.currentWindowScene() else {
497
+ let errorData = [
498
+ "responseCode": IapErrorCode.serviceError,
499
+ "debugMessage": "Could not find window scene",
500
+ "code": IapErrorCode.serviceError,
501
+ "message": "Could not find window scene",
502
+ "productId": sku,
503
+ ]
504
+ self.sendEvent(IapEvent.PurchaseError, errorData)
496
505
  throw Exception(name: "ExpoIapModule", description: "Could not find window scene", code: IapErrorCode.serviceError)
497
506
  }
498
507
  let result: Product.PurchaseResult
@@ -536,19 +545,105 @@ public class ExpoIapModule: Module {
536
545
  return serialized
537
546
  }
538
547
  case .userCancelled:
548
+ let errorData = [
549
+ "responseCode": IapErrorCode.userCancelled,
550
+ "debugMessage": "User cancelled the purchase",
551
+ "code": IapErrorCode.userCancelled,
552
+ "message": "User cancelled the purchase",
553
+ "productId": sku,
554
+ ]
555
+ self.sendEvent(IapEvent.PurchaseError, errorData)
539
556
  throw Exception(name: "ExpoIapModule", description: "User cancelled the purchase", code: IapErrorCode.userCancelled)
540
557
  case .pending:
558
+ let errorData = [
559
+ "responseCode": IapErrorCode.deferredPayment,
560
+ "debugMessage": "The payment was deferred",
561
+ "code": IapErrorCode.deferredPayment,
562
+ "message": "The payment was deferred",
563
+ "productId": sku,
564
+ ]
565
+ self.sendEvent(IapEvent.PurchaseError, errorData)
541
566
  throw Exception(name: "ExpoIapModule", description: "The payment was deferred", code: IapErrorCode.deferredPayment)
542
567
  @unknown default:
568
+ let errorData = [
569
+ "responseCode": IapErrorCode.unknown,
570
+ "debugMessage": "Unknown purchase result",
571
+ "code": IapErrorCode.unknown,
572
+ "message": "Unknown purchase result",
573
+ "productId": sku,
574
+ ]
575
+ self.sendEvent(IapEvent.PurchaseError, errorData)
543
576
  throw Exception(name: "ExpoIapModule", description: "Unknown purchase result", code: IapErrorCode.unknown)
544
577
  }
545
578
  } catch {
546
579
  if error is Exception {
547
580
  throw error
548
581
  }
549
- throw Exception(name: "ExpoIapModule", description: "Purchase failed: \(error.localizedDescription)", code: IapErrorCode.purchaseError)
582
+
583
+ // Map StoreKit errors to proper error codes
584
+ var errorCode = IapErrorCode.purchaseError
585
+ var errorMessage = error.localizedDescription
586
+
587
+ // Check for specific StoreKit error types
588
+ if let nsError = error as NSError? {
589
+ switch nsError.domain {
590
+ case "SKErrorDomain":
591
+ // Handle SKError codes
592
+ switch nsError.code {
593
+ case 0: // SKError.unknown
594
+ errorCode = IapErrorCode.unknown
595
+ case 1: // SKError.clientInvalid
596
+ errorCode = IapErrorCode.serviceError
597
+ case 2: // SKError.paymentCancelled
598
+ errorCode = IapErrorCode.userCancelled
599
+ errorMessage = "User cancelled the purchase"
600
+ case 3: // SKError.paymentInvalid
601
+ errorCode = IapErrorCode.userError
602
+ case 4: // SKError.paymentNotAllowed
603
+ errorCode = IapErrorCode.userError
604
+ errorMessage = "Payment not allowed"
605
+ case 5: // SKError.storeProductNotAvailable
606
+ errorCode = IapErrorCode.itemUnavailable
607
+ case 6: // SKError.cloudServicePermissionDenied
608
+ errorCode = IapErrorCode.serviceError
609
+ case 7: // SKError.cloudServiceNetworkConnectionFailed
610
+ errorCode = IapErrorCode.networkError
611
+ case 8: // SKError.cloudServiceRevoked
612
+ errorCode = IapErrorCode.serviceError
613
+ default:
614
+ errorCode = IapErrorCode.purchaseError
615
+ }
616
+ case "NSURLErrorDomain":
617
+ errorCode = IapErrorCode.networkError
618
+ errorMessage = "Network error: \(error.localizedDescription)"
619
+ default:
620
+ errorCode = IapErrorCode.purchaseError
621
+ }
622
+ } else if error.localizedDescription.lowercased().contains("network") {
623
+ errorCode = IapErrorCode.networkError
624
+ } else if error.localizedDescription.lowercased().contains("cancelled") {
625
+ errorCode = IapErrorCode.userCancelled
626
+ }
627
+
628
+ let errorData = [
629
+ "responseCode": errorCode,
630
+ "debugMessage": "Purchase failed: \(error.localizedDescription)",
631
+ "code": errorCode,
632
+ "message": errorMessage,
633
+ "productId": sku,
634
+ ]
635
+ self.sendEvent(IapEvent.PurchaseError, errorData)
636
+ throw Exception(name: "ExpoIapModule", description: errorMessage, code: errorCode)
550
637
  }
551
638
  } else {
639
+ let errorData = [
640
+ "responseCode": IapErrorCode.itemUnavailable,
641
+ "debugMessage": "Invalid product ID",
642
+ "code": IapErrorCode.itemUnavailable,
643
+ "message": "Invalid product ID",
644
+ "productId": sku,
645
+ ]
646
+ self.sendEvent(IapEvent.PurchaseError, errorData)
552
647
  throw Exception(name: "ExpoIapModule", description: "Invalid product ID", code: IapErrorCode.itemUnavailable)
553
648
  }
554
649
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "2.7.11",
3
+ "version": "2.7.13",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1 +1 @@
1
- {"root":["./src/withIAP.ts"],"version":"5.8.3"}
1
+ {"root":["./src/withIAP.ts"],"version":"5.9.2"}
@@ -28,8 +28,9 @@ export type ProductBase = {
28
28
  };
29
29
 
30
30
  export type PurchaseBase = {
31
- id: string;
32
- transactionId?: string;
31
+ id: string; // Transaction identifier - used by finishTransaction
32
+ productId: string; // Product identifier - which product was purchased
33
+ transactionId?: string; // @deprecated - use id instead
33
34
  transactionDate: number;
34
35
  transactionReceipt: string;
35
36
  };
@@ -0,0 +1,122 @@
1
+ import {Platform} from 'react-native';
2
+ import {getAvailablePurchases} from '../index';
3
+
4
+ export interface ActiveSubscription {
5
+ productId: string;
6
+ isActive: boolean;
7
+ expirationDateIOS?: Date;
8
+ autoRenewingAndroid?: boolean;
9
+ environmentIOS?: string;
10
+ willExpireSoon?: boolean;
11
+ daysUntilExpirationIOS?: number;
12
+ }
13
+
14
+ /**
15
+ * Get all active subscriptions with detailed information
16
+ * @param subscriptionIds - Optional array of subscription product IDs to filter. If not provided, returns all active subscriptions.
17
+ * @returns Promise<ActiveSubscription[]> array of active subscriptions with details
18
+ */
19
+ export const getActiveSubscriptions = async (
20
+ subscriptionIds?: string[],
21
+ ): Promise<ActiveSubscription[]> => {
22
+ try {
23
+ const purchases = await getAvailablePurchases();
24
+ const currentTime = Date.now();
25
+ const activeSubscriptions: ActiveSubscription[] = [];
26
+
27
+ // Filter purchases to find active subscriptions
28
+ const filteredPurchases = purchases.filter((purchase) => {
29
+ // If specific IDs provided, filter by them
30
+ if (subscriptionIds && subscriptionIds.length > 0) {
31
+ if (!subscriptionIds.includes(purchase.id)) {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ // Check if this purchase has subscription-specific fields
37
+ const hasSubscriptionFields =
38
+ ('expirationDateIos' in purchase && purchase.expirationDateIos) ||
39
+ 'autoRenewingAndroid' in purchase;
40
+
41
+ if (!hasSubscriptionFields) {
42
+ return false;
43
+ }
44
+
45
+ // Check if it's actually active
46
+ if (Platform.OS === 'ios') {
47
+ if ('expirationDateIos' in purchase && purchase.expirationDateIos) {
48
+ return purchase.expirationDateIos > currentTime;
49
+ }
50
+ if (
51
+ 'environmentIos' in purchase &&
52
+ purchase.environmentIos === 'Sandbox'
53
+ ) {
54
+ const dayInMs = 24 * 60 * 60 * 1000;
55
+ if (
56
+ purchase.transactionDate &&
57
+ currentTime - purchase.transactionDate < dayInMs
58
+ ) {
59
+ return true;
60
+ }
61
+ }
62
+ } else if (Platform.OS === 'android') {
63
+ // For Android, if it's in the purchases list, it's active
64
+ return true;
65
+ }
66
+
67
+ return false;
68
+ });
69
+
70
+ // Convert to ActiveSubscription format
71
+ for (const purchase of filteredPurchases) {
72
+ const subscription: ActiveSubscription = {
73
+ productId: purchase.id,
74
+ isActive: true,
75
+ };
76
+
77
+ // Add platform-specific details
78
+ if (Platform.OS === 'ios') {
79
+ if ('expirationDateIos' in purchase && purchase.expirationDateIos) {
80
+ const expirationDate = new Date(purchase.expirationDateIos);
81
+ subscription.expirationDateIOS = expirationDate;
82
+
83
+ // Calculate days until expiration
84
+ const daysUntilExpiration = Math.floor(
85
+ (purchase.expirationDateIos - currentTime) / (1000 * 60 * 60 * 24),
86
+ );
87
+ subscription.daysUntilExpirationIOS = daysUntilExpiration;
88
+ subscription.willExpireSoon = daysUntilExpiration <= 7;
89
+ }
90
+
91
+ if ('environmentIos' in purchase) {
92
+ subscription.environmentIOS = purchase.environmentIos;
93
+ }
94
+ } else if (Platform.OS === 'android') {
95
+ if ('autoRenewingAndroid' in purchase) {
96
+ subscription.autoRenewingAndroid = purchase.autoRenewingAndroid;
97
+ // If auto-renewing is false, subscription will expire soon
98
+ subscription.willExpireSoon = !purchase.autoRenewingAndroid;
99
+ }
100
+ }
101
+
102
+ activeSubscriptions.push(subscription);
103
+ }
104
+
105
+ return activeSubscriptions;
106
+ } catch (error) {
107
+ console.error('Error getting active subscriptions:', error);
108
+ return [];
109
+ }
110
+ };
111
+
112
+ /**
113
+ * Check if user has any active subscriptions
114
+ * @param subscriptionIds - Optional array of subscription product IDs to check. If not provided, checks all subscriptions.
115
+ * @returns Promise<boolean> true if user has at least one active subscription
116
+ */
117
+ export const hasActiveSubscriptions = async (
118
+ subscriptionIds?: string[],
119
+ ): Promise<boolean> => {
120
+ const subscriptions = await getActiveSubscriptions(subscriptionIds);
121
+ return subscriptions.length > 0;
122
+ };
package/src/index.ts CHANGED
@@ -36,6 +36,13 @@ export * from './modules/android';
36
36
  export * from './modules/ios';
37
37
  export type {AppTransactionIOS} from './types/ExpoIapIos.types';
38
38
 
39
+ // Export subscription helpers
40
+ export {
41
+ getActiveSubscriptions,
42
+ hasActiveSubscriptions,
43
+ type ActiveSubscription,
44
+ } from './helpers/subscription';
45
+
39
46
  // Get the native constant value
40
47
  export const PI = ExpoIapModule.PI;
41
48
 
@@ -326,8 +333,9 @@ export const getAvailablePurchases = ({
326
333
  ),
327
334
  android: async () => {
328
335
  const products = await ExpoIapModule.getAvailableItemsByType('inapp');
329
- const subscriptions =
330
- await ExpoIapModule.getAvailableItemsByType('subs');
336
+ const subscriptions = await ExpoIapModule.getAvailableItemsByType(
337
+ 'subs',
338
+ );
331
339
  return products.concat(subscriptions);
332
340
  },
333
341
  }) || (() => Promise.resolve([]))
@@ -555,10 +563,10 @@ export const finishTransaction = ({
555
563
  return (
556
564
  Platform.select({
557
565
  ios: async () => {
558
- const transactionId = purchase.transactionId;
566
+ const transactionId = purchase.id;
559
567
  if (!transactionId) {
560
568
  return Promise.reject(
561
- new Error('transactionId required to finish iOS transaction'),
569
+ new Error('purchase.id required to finish iOS transaction'),
562
570
  );
563
571
  }
564
572
  await ExpoIapModule.finishTransaction(transactionId);
package/src/useIap.ts CHANGED
@@ -16,6 +16,9 @@ import {
16
16
  requestPurchase as requestPurchaseInternal,
17
17
  requestProducts,
18
18
  validateReceipt as validateReceiptInternal,
19
+ getActiveSubscriptions,
20
+ hasActiveSubscriptions,
21
+ type ActiveSubscription,
19
22
  } from './';
20
23
  import {
21
24
  syncIOS,
@@ -47,6 +50,7 @@ type UseIap = {
47
50
  currentPurchase?: ProductPurchase;
48
51
  currentPurchaseError?: PurchaseError;
49
52
  promotedProductIOS?: Product;
53
+ activeSubscriptions: ActiveSubscription[];
50
54
  clearCurrentPurchase: () => void;
51
55
  clearCurrentPurchaseError: () => void;
52
56
  finishTransaction: ({
@@ -88,6 +92,10 @@ type UseIap = {
88
92
  restorePurchases: () => Promise<void>; // 구매 복원 함수 추가
89
93
  getPromotedProductIOS: () => Promise<any | null>;
90
94
  buyPromotedProductIOS: () => Promise<void>;
95
+ getActiveSubscriptions: (
96
+ subscriptionIds?: string[],
97
+ ) => Promise<ActiveSubscription[]>;
98
+ hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
91
99
  };
92
100
 
93
101
  export interface UseIAPOptions {
@@ -116,6 +124,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
116
124
  const [currentPurchaseError, setCurrentPurchaseError] =
117
125
  useState<PurchaseError>();
118
126
  const [promotedProductIdIOS] = useState<string>();
127
+ const [activeSubscriptions, setActiveSubscriptions] = useState<
128
+ ActiveSubscription[]
129
+ >([]);
119
130
 
120
131
  const optionsRef = useRef<UseIAPOptions | undefined>(options);
121
132
 
@@ -241,6 +252,33 @@ export function useIAP(options?: UseIAPOptions): UseIap {
241
252
  }
242
253
  }, []);
243
254
 
255
+ const getActiveSubscriptionsInternal = useCallback(
256
+ async (subscriptionIds?: string[]): Promise<ActiveSubscription[]> => {
257
+ try {
258
+ const result = await getActiveSubscriptions(subscriptionIds);
259
+ setActiveSubscriptions(result);
260
+ return result;
261
+ } catch (error) {
262
+ console.error('Error getting active subscriptions:', error);
263
+ setActiveSubscriptions([]);
264
+ return [];
265
+ }
266
+ },
267
+ [],
268
+ );
269
+
270
+ const hasActiveSubscriptionsInternal = useCallback(
271
+ async (subscriptionIds?: string[]): Promise<boolean> => {
272
+ try {
273
+ return await hasActiveSubscriptions(subscriptionIds);
274
+ } catch (error) {
275
+ console.error('Error checking active subscriptions:', error);
276
+ return false;
277
+ }
278
+ },
279
+ [],
280
+ );
281
+
244
282
  const getPurchaseHistoriesInternal = useCallback(async (): Promise<void> => {
245
283
  setPurchaseHistories(await getPurchaseHistories());
246
284
  }, []);
@@ -408,6 +446,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
408
446
  currentPurchase,
409
447
  currentPurchaseError,
410
448
  promotedProductIOS,
449
+ activeSubscriptions,
411
450
  clearCurrentPurchase,
412
451
  clearCurrentPurchaseError,
413
452
  getAvailablePurchases: getAvailablePurchasesInternal,
@@ -420,5 +459,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
420
459
  getSubscriptions: getSubscriptionsInternal,
421
460
  getPromotedProductIOS,
422
461
  buyPromotedProductIOS,
462
+ getActiveSubscriptions: getActiveSubscriptionsInternal,
463
+ hasActiveSubscriptions: hasActiveSubscriptionsInternal,
423
464
  };
424
465
  }
package/bun.lockb DELETED
Binary file