expo-iap 2.9.0-rc.3 → 2.9.0

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 (46) hide show
  1. package/CHANGELOG.md +29 -6
  2. package/CLAUDE.md +40 -0
  3. package/CONTRIBUTING.md +4 -3
  4. package/build/helpers/subscription.d.ts +3 -0
  5. package/build/helpers/subscription.d.ts.map +1 -1
  6. package/build/helpers/subscription.js +10 -5
  7. package/build/helpers/subscription.js.map +1 -1
  8. package/build/index.d.ts.map +1 -1
  9. package/build/index.js +29 -10
  10. package/build/index.js.map +1 -1
  11. package/build/modules/ios.d.ts +4 -5
  12. package/build/modules/ios.d.ts.map +1 -1
  13. package/build/modules/ios.js +2 -3
  14. package/build/modules/ios.js.map +1 -1
  15. package/build/types/ExpoIapAndroid.types.d.ts +2 -2
  16. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
  17. package/build/types/ExpoIapAndroid.types.js.map +1 -1
  18. package/build/types/ExpoIapIOS.types.d.ts +3 -3
  19. package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
  20. package/build/types/ExpoIapIOS.types.js.map +1 -1
  21. package/build/useIAP.d.ts +1 -1
  22. package/build/useIAP.d.ts.map +1 -1
  23. package/build/useIAP.js +54 -33
  24. package/build/useIAP.js.map +1 -1
  25. package/build/utils/constants.d.ts +4 -0
  26. package/build/utils/constants.d.ts.map +1 -0
  27. package/build/utils/constants.js +12 -0
  28. package/build/utils/constants.js.map +1 -0
  29. package/ios/ExpoIap.podspec +1 -1
  30. package/ios/ExpoIapModule.swift +341 -334
  31. package/jest.config.js +19 -14
  32. package/package.json +2 -3
  33. package/plugin/build/withIAP.js +25 -23
  34. package/plugin/build/withLocalOpenIAP.js +5 -1
  35. package/plugin/src/withIAP.ts +39 -31
  36. package/plugin/src/withLocalOpenIAP.ts +8 -2
  37. package/plugin/tsconfig.tsbuildinfo +1 -1
  38. package/src/helpers/subscription.ts +35 -23
  39. package/src/index.ts +50 -33
  40. package/src/modules/ios.ts +4 -5
  41. package/src/types/ExpoIapAndroid.types.ts +4 -3
  42. package/src/types/ExpoIapIOS.types.ts +3 -4
  43. package/src/useIAP.ts +73 -52
  44. package/src/utils/constants.ts +14 -0
  45. package/ios/ProductStore.swift +0 -27
  46. package/ios/Types.swift +0 -96
package/jest.config.js CHANGED
@@ -1,21 +1,26 @@
1
1
  module.exports = {
2
2
  preset: 'ts-jest',
3
3
  testEnvironment: 'node',
4
+ // Disable watchman to avoid sandbox/permission issues in CI and sandboxes
5
+ watchman: false,
4
6
  roots: ['<rootDir>/src'],
5
7
  testMatch: [
6
8
  '**/__tests__/**/*.+(ts|tsx|js)',
7
- '**/?(*.)+(spec|test).+(ts|tsx|js)'
9
+ '**/?(*.)+(spec|test).+(ts|tsx|js)',
8
10
  ],
9
11
  transform: {
10
- '^.+\\.(ts|tsx)$': ['ts-jest', {
11
- tsconfig: {
12
- jsx: 'react',
13
- esModuleInterop: true,
14
- allowSyntheticDefaultImports: true,
15
- moduleResolution: 'node',
16
- skipLibCheck: true,
17
- }
18
- }]
12
+ '^.+\\.(ts|tsx)$': [
13
+ 'ts-jest',
14
+ {
15
+ tsconfig: {
16
+ jsx: 'react',
17
+ esModuleInterop: true,
18
+ allowSyntheticDefaultImports: true,
19
+ moduleResolution: 'node',
20
+ skipLibCheck: true,
21
+ },
22
+ },
23
+ ],
19
24
  },
20
25
  moduleNameMapper: {
21
26
  '^react-native$': '<rootDir>/src/__mocks__/react-native.js',
@@ -34,7 +39,7 @@ module.exports = {
34
39
  branches: 15,
35
40
  functions: 15,
36
41
  lines: 15,
37
- statements: 15
38
- }
39
- }
40
- };
42
+ statements: 15,
43
+ },
44
+ },
45
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "2.9.0-rc.3",
3
+ "version": "2.9.0",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -61,6 +61,5 @@
61
61
  },
62
62
  "expo": {
63
63
  "plugin": "./app.plugin.js"
64
- },
65
- "packageManager": "yarn@3.6.1+sha512.de524adec81a6c3d7a26d936d439d2832e351cdfc5728f9d91f3fc85dd20b04391c038e9b4ecab11cae2b0dd9f0d55fd355af766bc5c1a7f8d25d96bb2a0b2ca"
64
+ }
66
65
  }
@@ -41,8 +41,16 @@ const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
42
  const withLocalOpenIAP_1 = __importDefault(require("./withLocalOpenIAP"));
43
43
  const pkg = require('../../package.json');
44
- // Global flag to prevent duplicate logs
45
- let hasLoggedPluginExecution = false;
44
+ // Log a message only once per Node process
45
+ const logOnce = (() => {
46
+ const printed = new Set();
47
+ return (msg) => {
48
+ if (!printed.has(msg)) {
49
+ console.log(msg);
50
+ printed.add(msg);
51
+ }
52
+ };
53
+ })();
46
54
  const addLineToGradle = (content, anchor, lineToAdd, offset = 1) => {
47
55
  const lines = content.split('\n');
48
56
  const index = lines.findIndex((line) => line.match(anchor));
@@ -70,9 +78,8 @@ const modifyAppBuildGradle = (gradle) => {
70
78
  hasAddedDependency = true;
71
79
  }
72
80
  // Log only once and only if we actually added dependencies
73
- if (hasAddedDependency && !hasLoggedPluginExecution) {
74
- console.log('🛠️ expo-iap: Added billing dependencies to build.gradle');
75
- }
81
+ if (hasAddedDependency)
82
+ logOnce('🛠️ expo-iap: Added billing dependencies to build.gradle');
76
83
  return modified;
77
84
  };
78
85
  const withIapAndroid = (config) => {
@@ -91,14 +98,10 @@ const withIapAndroid = (config) => {
91
98
  const alreadyExists = permissions.some((p) => p.$['android:name'] === 'com.android.vending.BILLING');
92
99
  if (!alreadyExists) {
93
100
  permissions.push(billingPerm);
94
- if (!hasLoggedPluginExecution) {
95
- console.log('✅ Added com.android.vending.BILLING to AndroidManifest.xml');
96
- }
101
+ logOnce('✅ Added com.android.vending.BILLING to AndroidManifest.xml');
97
102
  }
98
103
  else {
99
- if (!hasLoggedPluginExecution) {
100
- console.log('ℹ️ com.android.vending.BILLING already exists in AndroidManifest.xml');
101
- }
104
+ logOnce('ℹ️ com.android.vending.BILLING already exists in AndroidManifest.xml');
102
105
  }
103
106
  return config;
104
107
  });
@@ -119,17 +122,13 @@ const withIapIOS = (config) => {
119
122
  const cdnLine = `source 'https://cdn.cocoapods.org/'`;
120
123
  if (!content.includes(cdnLine)) {
121
124
  content = `${cdnLine}\n\n${content}`;
122
- if (!hasLoggedPluginExecution) {
123
- console.log('📦 expo-iap: Added CocoaPods CDN source to Podfile');
124
- }
125
+ logOnce('📦 expo-iap: Added CocoaPods CDN source to Podfile');
125
126
  }
126
127
  // 2) Remove any lingering local OpenIAP pod injection
127
128
  const localPodRegex = /^\s*pod\s+'openiap'\s*,\s*:path\s*=>\s*['"][^'"]+['"][^\n]*$/gm;
128
129
  if (localPodRegex.test(content)) {
129
130
  content = content.replace(localPodRegex, '').replace(/\n{3,}/g, '\n\n');
130
- if (!hasLoggedPluginExecution) {
131
- console.log('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
132
- }
131
+ logOnce('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
133
132
  }
134
133
  fs.writeFileSync(podfilePath, content);
135
134
  return config;
@@ -142,17 +141,20 @@ const withIap = (config, options) => {
142
141
  let result = withIapAndroid(config);
143
142
  // iOS: choose one path to avoid overlap
144
143
  if (options?.enableLocalDev || options?.localPath) {
145
- const localPath = options.localPath || '/Users/crossplatformkorea/Github/hyodotdev/openiap-apple';
146
- console.log(`🔧 [expo-iap] Enabling local OpenIAP development at: ${localPath}`);
147
- result = (0, withLocalOpenIAP_1.default)(result, { localPath });
144
+ if (!options?.localPath) {
145
+ config_plugins_1.WarningAggregator.addWarningIOS('expo-iap', 'enableLocalDev is true but no localPath provided. Skipping local OpenIAP integration.');
146
+ }
147
+ else {
148
+ const localPath = path.resolve(options.localPath);
149
+ logOnce(`🔧 [expo-iap] Enabling local OpenIAP development at: ${localPath}`);
150
+ result = (0, withLocalOpenIAP_1.default)(result, { localPath });
151
+ }
148
152
  }
149
153
  else {
150
154
  // Ensure iOS Podfile is set up to resolve public CocoaPods specs
151
155
  result = withIapIOS(result);
152
- console.log('📦 [expo-iap] Using OpenIAP from CocoaPods');
156
+ logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
153
157
  }
154
- // Set flag after first execution to prevent duplicate logs
155
- hasLoggedPluginExecution = true;
156
158
  return result;
157
159
  }
158
160
  catch (error) {
@@ -48,7 +48,7 @@ const withLocalOpenIAP = (config, props) => {
48
48
  const podfilePath = path.join(platformProjectRoot, 'Podfile');
49
49
  // Default local path or use provided one
50
50
  const localOpenIapPath = props?.localPath ||
51
- '/Users/crossplatformkorea/Github/hyodotdev/openiap-apple';
51
+ path.resolve(config.modRequest.projectRoot, 'openiap-apple');
52
52
  // Check if local path exists
53
53
  if (!fs.existsSync(localOpenIapPath)) {
54
54
  console.warn(`⚠️ Local openiap-apple path not found: ${localOpenIapPath}`);
@@ -56,6 +56,10 @@ const withLocalOpenIAP = (config, props) => {
56
56
  return config;
57
57
  }
58
58
  // Read Podfile
59
+ if (!fs.existsSync(podfilePath)) {
60
+ console.warn(`⚠️ Podfile not found at ${podfilePath}. Skipping.`);
61
+ return config;
62
+ }
59
63
  let podfileContent = fs.readFileSync(podfilePath, 'utf8');
60
64
  // Check if already has the local pod reference
61
65
  if (podfileContent.includes("pod 'openiap',")) {
@@ -12,8 +12,16 @@ import withLocalOpenIAP from './withLocalOpenIAP';
12
12
 
13
13
  const pkg = require('../../package.json');
14
14
 
15
- // Global flag to prevent duplicate logs
16
- let hasLoggedPluginExecution = false;
15
+ // Log a message only once per Node process
16
+ const logOnce = (() => {
17
+ const printed = new Set<string>();
18
+ return (msg: string) => {
19
+ if (!printed.has(msg)) {
20
+ console.log(msg);
21
+ printed.add(msg);
22
+ }
23
+ };
24
+ })();
17
25
 
18
26
  const addLineToGradle = (
19
27
  content: string,
@@ -53,9 +61,8 @@ const modifyAppBuildGradle = (gradle: string): string => {
53
61
  }
54
62
 
55
63
  // Log only once and only if we actually added dependencies
56
- if (hasAddedDependency && !hasLoggedPluginExecution) {
57
- console.log('🛠️ expo-iap: Added billing dependencies to build.gradle');
58
- }
64
+ if (hasAddedDependency)
65
+ logOnce('🛠️ expo-iap: Added billing dependencies to build.gradle');
59
66
 
60
67
  return modified;
61
68
  };
@@ -83,17 +90,11 @@ const withIapAndroid: ConfigPlugin = (config) => {
83
90
  );
84
91
  if (!alreadyExists) {
85
92
  permissions.push(billingPerm);
86
- if (!hasLoggedPluginExecution) {
87
- console.log(
88
- '✅ Added com.android.vending.BILLING to AndroidManifest.xml',
89
- );
90
- }
93
+ logOnce('✅ Added com.android.vending.BILLING to AndroidManifest.xml');
91
94
  } else {
92
- if (!hasLoggedPluginExecution) {
93
- console.log(
94
- 'ℹ️ com.android.vending.BILLING already exists in AndroidManifest.xml',
95
- );
96
- }
95
+ logOnce(
96
+ 'ℹ️ com.android.vending.BILLING already exists in AndroidManifest.xml',
97
+ );
97
98
  }
98
99
 
99
100
  return config;
@@ -107,7 +108,7 @@ const withIapIOS: ConfigPlugin = (config) => {
107
108
  return withDangerousMod(config, [
108
109
  'ios',
109
110
  async (config) => {
110
- const { platformProjectRoot } = config.modRequest;
111
+ const {platformProjectRoot} = config.modRequest;
111
112
  const podfilePath = path.join(platformProjectRoot, 'Podfile');
112
113
 
113
114
  if (!fs.existsSync(podfilePath)) {
@@ -120,18 +121,15 @@ const withIapIOS: ConfigPlugin = (config) => {
120
121
  const cdnLine = `source 'https://cdn.cocoapods.org/'`;
121
122
  if (!content.includes(cdnLine)) {
122
123
  content = `${cdnLine}\n\n${content}`;
123
- if (!hasLoggedPluginExecution) {
124
- console.log('📦 expo-iap: Added CocoaPods CDN source to Podfile');
125
- }
124
+ logOnce('📦 expo-iap: Added CocoaPods CDN source to Podfile');
126
125
  }
127
126
 
128
127
  // 2) Remove any lingering local OpenIAP pod injection
129
- const localPodRegex = /^\s*pod\s+'openiap'\s*,\s*:path\s*=>\s*['"][^'"]+['"][^\n]*$/gm;
128
+ const localPodRegex =
129
+ /^\s*pod\s+'openiap'\s*,\s*:path\s*=>\s*['"][^'"]+['"][^\n]*$/gm;
130
130
  if (localPodRegex.test(content)) {
131
131
  content = content.replace(localPodRegex, '').replace(/\n{3,}/g, '\n\n');
132
- if (!hasLoggedPluginExecution) {
133
- console.log('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
134
- }
132
+ logOnce('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
135
133
  }
136
134
 
137
135
  fs.writeFileSync(podfilePath, content);
@@ -147,24 +145,34 @@ export interface ExpoIapPluginOptions {
147
145
  enableLocalDev?: boolean;
148
146
  }
149
147
 
150
- const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (config, options) => {
148
+ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
149
+ config,
150
+ options,
151
+ ) => {
151
152
  try {
152
153
  // Apply Android modifications
153
154
  let result = withIapAndroid(config);
154
155
 
155
156
  // iOS: choose one path to avoid overlap
156
157
  if (options?.enableLocalDev || options?.localPath) {
157
- const localPath = options.localPath || '/Users/crossplatformkorea/Github/hyodotdev/openiap-apple';
158
- console.log(`🔧 [expo-iap] Enabling local OpenIAP development at: ${localPath}`);
159
- result = withLocalOpenIAP(result, { localPath });
158
+ if (!options?.localPath) {
159
+ WarningAggregator.addWarningIOS(
160
+ 'expo-iap',
161
+ 'enableLocalDev is true but no localPath provided. Skipping local OpenIAP integration.',
162
+ );
163
+ } else {
164
+ const localPath = path.resolve(options.localPath);
165
+ logOnce(
166
+ `🔧 [expo-iap] Enabling local OpenIAP development at: ${localPath}`,
167
+ );
168
+ result = withLocalOpenIAP(result, {localPath});
169
+ }
160
170
  } else {
161
171
  // Ensure iOS Podfile is set up to resolve public CocoaPods specs
162
172
  result = withIapIOS(result);
163
- console.log('📦 [expo-iap] Using OpenIAP from CocoaPods');
173
+ logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
164
174
  }
165
-
166
- // Set flag after first execution to prevent duplicate logs
167
- hasLoggedPluginExecution = true;
175
+
168
176
  return result;
169
177
  } catch (error) {
170
178
  WarningAggregator.addWarningAndroid(
@@ -19,11 +19,13 @@ const withLocalOpenIAP: ConfigPlugin<{localPath?: string} | void> = (
19
19
  // Default local path or use provided one
20
20
  const localOpenIapPath =
21
21
  props?.localPath ||
22
- '/Users/crossplatformkorea/Github/hyodotdev/openiap-apple';
22
+ path.resolve(config.modRequest.projectRoot, 'openiap-apple');
23
23
 
24
24
  // Check if local path exists
25
25
  if (!fs.existsSync(localOpenIapPath)) {
26
- console.warn(`⚠️ Local openiap-apple path not found: ${localOpenIapPath}`);
26
+ console.warn(
27
+ `⚠️ Local openiap-apple path not found: ${localOpenIapPath}`,
28
+ );
27
29
  console.warn(
28
30
  ' Skipping local pod injection. Using default pod resolution.',
29
31
  );
@@ -31,6 +33,10 @@ const withLocalOpenIAP: ConfigPlugin<{localPath?: string} | void> = (
31
33
  }
32
34
 
33
35
  // Read Podfile
36
+ if (!fs.existsSync(podfilePath)) {
37
+ console.warn(`⚠️ Podfile not found at ${podfilePath}. Skipping.`);
38
+ return config;
39
+ }
34
40
  let podfileContent = fs.readFileSync(podfilePath, 'utf8');
35
41
 
36
42
  // Check if already has the local pod reference
@@ -1 +1 @@
1
- {"root":["./src/withiap.ts","./src/withlocalopeniap.ts"],"version":"5.9.2"}
1
+ {"root":["./src/withIAP.ts","./src/withLocalOpenIAP.ts"],"version":"5.9.2"}
@@ -1,9 +1,12 @@
1
- import { Platform } from 'react-native';
2
- import { getAvailablePurchases } from '../index';
1
+ import {Platform} from 'react-native';
2
+ import {getAvailablePurchases} from '../index';
3
3
 
4
4
  export interface ActiveSubscription {
5
5
  productId: string;
6
6
  isActive: boolean;
7
+ transactionId: string; // Transaction identifier for backend validation
8
+ purchaseToken?: string; // JWT token (iOS) or purchase token (Android) for backend validation
9
+ transactionDate: number; // Transaction timestamp
7
10
  expirationDateIOS?: Date;
8
11
  autoRenewingAndroid?: boolean;
9
12
  environmentIOS?: string;
@@ -17,13 +20,13 @@ export interface ActiveSubscription {
17
20
  * @returns Promise<ActiveSubscription[]> array of active subscriptions with details
18
21
  */
19
22
  export const getActiveSubscriptions = async (
20
- subscriptionIds?: string[]
23
+ subscriptionIds?: string[],
21
24
  ): Promise<ActiveSubscription[]> => {
22
25
  try {
23
26
  const purchases = await getAvailablePurchases();
24
27
  const currentTime = Date.now();
25
28
  const activeSubscriptions: ActiveSubscription[] = [];
26
-
29
+
27
30
  // Filter purchases to find active subscriptions
28
31
  const filteredPurchases = purchases.filter((purchase) => {
29
32
  // If specific IDs provided, filter by them
@@ -32,17 +35,16 @@ export const getActiveSubscriptions = async (
32
35
  return false;
33
36
  }
34
37
  }
35
-
38
+
36
39
  // Check if this purchase has subscription-specific fields
37
- const hasSubscriptionFields =
38
- ('expirationDateIOS' in purchase && purchase.expirationDateIOS) ||
39
- ('autoRenewingAndroid' in purchase) ||
40
- ('environmentIOS' in purchase && purchase.environmentIOS === 'Sandbox');
41
-
40
+ const hasSubscriptionFields =
41
+ ('expirationDateIOS' in purchase && !!purchase.expirationDateIOS) ||
42
+ 'autoRenewingAndroid' in purchase;
43
+
42
44
  if (!hasSubscriptionFields) {
43
45
  return false;
44
46
  }
45
-
47
+
46
48
  // Check if it's actually active
47
49
  if (Platform.OS === 'ios') {
48
50
  if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
@@ -53,8 +55,15 @@ export const getActiveSubscriptions = async (
53
55
  if ('environmentIOS' in purchase && purchase.environmentIOS) {
54
56
  const dayInMs = 24 * 60 * 60 * 1000;
55
57
  // If no expiration date, consider active if transaction is recent (within 24 hours for Sandbox)
56
- if (!('expirationDateIOS' in purchase) || !purchase.expirationDateIOS) {
57
- if (purchase.environmentIOS === 'Sandbox' && purchase.transactionDate && (currentTime - purchase.transactionDate) < dayInMs) {
58
+ if (
59
+ !('expirationDateIOS' in purchase) ||
60
+ !purchase.expirationDateIOS
61
+ ) {
62
+ if (
63
+ purchase.environmentIOS === 'Sandbox' &&
64
+ purchase.transactionDate &&
65
+ currentTime - purchase.transactionDate < dayInMs
66
+ ) {
58
67
  return true;
59
68
  }
60
69
  }
@@ -63,31 +72,34 @@ export const getActiveSubscriptions = async (
63
72
  // For Android, if it's in the purchases list, it's active
64
73
  return true;
65
74
  }
66
-
75
+
67
76
  return false;
68
77
  });
69
-
78
+
70
79
  // Convert to ActiveSubscription format
71
80
  for (const purchase of filteredPurchases) {
72
81
  const subscription: ActiveSubscription = {
73
82
  productId: purchase.productId,
74
83
  isActive: true,
84
+ transactionId: purchase.transactionId || purchase.id,
85
+ purchaseToken: purchase.purchaseToken,
86
+ transactionDate: purchase.transactionDate,
75
87
  };
76
-
88
+
77
89
  // Add platform-specific details
78
90
  if (Platform.OS === 'ios') {
79
91
  if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
80
92
  const expirationDate = new Date(purchase.expirationDateIOS);
81
93
  subscription.expirationDateIOS = expirationDate;
82
-
94
+
83
95
  // Calculate days until expiration (round to nearest day)
84
96
  const daysUntilExpiration = Math.round(
85
- (purchase.expirationDateIOS - currentTime) / (1000 * 60 * 60 * 24)
97
+ (purchase.expirationDateIOS - currentTime) / (1000 * 60 * 60 * 24),
86
98
  );
87
99
  subscription.daysUntilExpirationIOS = daysUntilExpiration;
88
100
  subscription.willExpireSoon = daysUntilExpiration <= 7;
89
101
  }
90
-
102
+
91
103
  if ('environmentIOS' in purchase) {
92
104
  subscription.environmentIOS = purchase.environmentIOS;
93
105
  }
@@ -98,10 +110,10 @@ export const getActiveSubscriptions = async (
98
110
  subscription.willExpireSoon = !purchase.autoRenewingAndroid;
99
111
  }
100
112
  }
101
-
113
+
102
114
  activeSubscriptions.push(subscription);
103
115
  }
104
-
116
+
105
117
  return activeSubscriptions;
106
118
  } catch (error) {
107
119
  console.error('Error getting active subscriptions:', error);
@@ -115,8 +127,8 @@ export const getActiveSubscriptions = async (
115
127
  * @returns Promise<boolean> true if user has at least one active subscription
116
128
  */
117
129
  export const hasActiveSubscriptions = async (
118
- subscriptionIds?: string[]
130
+ subscriptionIds?: string[],
119
131
  ): Promise<boolean> => {
120
132
  const subscriptions = await getActiveSubscriptions(subscriptionIds);
121
133
  return subscriptions.length > 0;
122
- };
134
+ };
package/src/index.ts CHANGED
@@ -72,17 +72,33 @@ export const emitter = (ExpoIapModule || NativeModulesProxy.ExpoIap) as {
72
72
  export const purchaseUpdatedListener = (
73
73
  listener: (event: Purchase) => void,
74
74
  ) => {
75
+ console.log('[JS] Registering purchaseUpdatedListener');
76
+ const wrappedListener = (event: Purchase) => {
77
+ console.log('[JS] purchaseUpdatedListener fired:', event);
78
+ listener(event);
79
+ };
75
80
  const emitterSubscription = emitter.addListener(
76
81
  OpenIapEvent.PurchaseUpdated,
77
- listener,
82
+ wrappedListener,
78
83
  );
84
+ console.log('[JS] purchaseUpdatedListener registered successfully');
79
85
  return emitterSubscription;
80
86
  };
81
87
 
82
88
  export const purchaseErrorListener = (
83
89
  listener: (error: PurchaseError) => void,
84
90
  ) => {
85
- return emitter.addListener(OpenIapEvent.PurchaseError, listener);
91
+ console.log('[JS] Registering purchaseErrorListener');
92
+ const wrappedListener = (error: PurchaseError) => {
93
+ console.log('[JS] purchaseErrorListener fired:', error);
94
+ listener(error);
95
+ };
96
+ const emitterSubscription = emitter.addListener(
97
+ OpenIapEvent.PurchaseError,
98
+ wrappedListener,
99
+ );
100
+ console.log('[JS] purchaseErrorListener registered successfully');
101
+ return emitterSubscription;
86
102
  };
87
103
 
88
104
  /**
@@ -233,16 +249,19 @@ export const fetchProducts = async ({
233
249
  }
234
250
 
235
251
  if (Platform.OS === 'ios') {
236
- const rawItems = await ExpoIapModule.fetchProducts(skus);
252
+ const rawItems = await ExpoIapModule.fetchProducts({skus, type});
253
+
237
254
  const filteredItems = rawItems.filter((item: unknown) => {
238
- if (!isProductIOS(item)) return false;
239
- return (
255
+ if (!isProductIOS(item)) {
256
+ return false;
257
+ }
258
+ const isValid =
240
259
  typeof item === 'object' &&
241
260
  item !== null &&
242
261
  'id' in item &&
243
262
  typeof item.id === 'string' &&
244
- skus.includes(item.id)
245
- );
263
+ skus.includes(item.id);
264
+ return isValid;
246
265
  });
247
266
 
248
267
  return type === 'inapp'
@@ -273,11 +292,11 @@ export const fetchProducts = async ({
273
292
 
274
293
  /**
275
294
  * @deprecated Use `fetchProducts` instead. This method will be removed in version 3.0.0.
276
- *
295
+ *
277
296
  * The 'request' prefix should only be used for event-based operations that trigger
278
297
  * purchase flows. Since this function simply fetches product information, it has been
279
298
  * renamed to `fetchProducts` to follow OpenIAP terminology guidelines.
280
- *
299
+ *
281
300
  * @example
282
301
  * ```typescript
283
302
  * // Old way (deprecated)
@@ -285,7 +304,7 @@ export const fetchProducts = async ({
285
304
  * skus: ['com.example.product1'],
286
305
  * type: 'inapp'
287
306
  * });
288
- *
307
+ *
289
308
  * // New way (recommended)
290
309
  * const products = await fetchProducts({
291
310
  * skus: ['com.example.product1'],
@@ -301,9 +320,9 @@ export const requestProducts = async ({
301
320
  type?: 'inapp' | 'subs';
302
321
  }): Promise<Product[] | SubscriptionProduct[]> => {
303
322
  console.warn(
304
- "`requestProducts` is deprecated. Use `fetchProducts` instead. The 'request' prefix should only be used for event-based operations. This method will be removed in version 3.0.0."
323
+ "`requestProducts` is deprecated. Use `fetchProducts` instead. The 'request' prefix should only be used for event-based operations. This method will be removed in version 3.0.0.",
305
324
  );
306
- return fetchProducts({ skus, type });
325
+ return fetchProducts({skus, type});
307
326
  };
308
327
 
309
328
  /**
@@ -326,8 +345,10 @@ export const getPurchaseHistory = ({
326
345
  '`getPurchaseHistory` is deprecated. Use `getPurchaseHistories` instead. This function will be removed in version 3.0.0.',
327
346
  );
328
347
  return getPurchaseHistories({
329
- alsoPublishToEventListenerIOS: alsoPublishToEventListenerIOS ?? alsoPublishToEventListener,
330
- onlyIncludeActiveItemsIOS: onlyIncludeActiveItemsIOS ?? onlyIncludeActiveItems,
348
+ alsoPublishToEventListenerIOS:
349
+ alsoPublishToEventListenerIOS ?? alsoPublishToEventListener,
350
+ onlyIncludeActiveItemsIOS:
351
+ onlyIncludeActiveItemsIOS ?? onlyIncludeActiveItems,
331
352
  });
332
353
  };
333
354
 
@@ -390,8 +411,9 @@ export const getAvailablePurchases = ({
390
411
  ),
391
412
  android: async () => {
392
413
  const products = await ExpoIapModule.getAvailableItemsByType('inapp');
393
- const subscriptions =
394
- await ExpoIapModule.getAvailableItemsByType('subs');
414
+ const subscriptions = await ExpoIapModule.getAvailableItemsByType(
415
+ 'subs',
416
+ );
395
417
  return products.concat(subscriptions);
396
418
  },
397
419
  }) || (() => Promise.resolve([]))
@@ -465,11 +487,7 @@ const normalizeRequestProps = (
465
487
  */
466
488
  export const requestPurchase = (
467
489
  requestObj: PurchaseRequest,
468
- ): Promise<
469
- | Purchase
470
- | Purchase[]
471
- | void
472
- > => {
490
+ ): Promise<Purchase | Purchase[] | void> => {
473
491
  const {request, type = 'inapp'} = requestObj;
474
492
 
475
493
  if (Platform.OS === 'ios') {
@@ -491,17 +509,15 @@ export const requestPurchase = (
491
509
 
492
510
  return (async () => {
493
511
  const offer = offerToRecordIOS(withOffer);
494
- const purchase = await ExpoIapModule.requestPurchase(
512
+ const purchase = await ExpoIapModule.requestPurchase({
495
513
  sku,
496
514
  andDangerouslyFinishTransactionAutomatically,
497
515
  appAccountToken,
498
- quantity ?? -1,
499
- offer,
500
- );
516
+ quantity,
517
+ withOffer: offer,
518
+ });
501
519
 
502
- return type === 'inapp'
503
- ? (purchase as Purchase)
504
- : (purchase as Purchase);
520
+ return type === 'inapp' ? (purchase as Purchase) : (purchase as Purchase);
505
521
  })();
506
522
  }
507
523
 
@@ -629,10 +645,11 @@ export const finishTransaction = ({
629
645
  },
630
646
  android: async () => {
631
647
  const androidPurchase = purchase as PurchaseAndroid;
632
-
648
+
633
649
  // Use purchaseToken if available, fallback to purchaseTokenAndroid for backward compatibility
634
- const token = androidPurchase.purchaseToken || androidPurchase.purchaseTokenAndroid;
635
-
650
+ const token =
651
+ androidPurchase.purchaseToken || androidPurchase.purchaseTokenAndroid;
652
+
636
653
  if (!token) {
637
654
  return Promise.reject(
638
655
  new PurchaseError(
@@ -642,8 +659,8 @@ export const finishTransaction = ({
642
659
  undefined,
643
660
  'E_DEVELOPER_ERROR' as ErrorCode,
644
661
  androidPurchase.productId,
645
- 'android'
646
- )
662
+ 'android',
663
+ ),
647
664
  );
648
665
  }
649
666