expo-iap 2.9.0-rc.3 → 2.9.0-rc.4
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 +29 -6
- package/CLAUDE.md +40 -0
- package/CONTRIBUTING.md +4 -3
- package/build/helpers/subscription.d.ts +3 -0
- package/build/helpers/subscription.d.ts.map +1 -1
- package/build/helpers/subscription.js +9 -3
- package/build/helpers/subscription.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +29 -10
- package/build/index.js.map +1 -1
- package/build/modules/ios.d.ts +4 -5
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +2 -3
- package/build/modules/ios.js.map +1 -1
- package/build/types/ExpoIapAndroid.types.d.ts +2 -2
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
- package/build/types/ExpoIapAndroid.types.js.map +1 -1
- package/build/types/ExpoIapIOS.types.d.ts +3 -3
- 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 -1
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +54 -33
- package/build/useIAP.js.map +1 -1
- package/build/utils/constants.d.ts +4 -0
- package/build/utils/constants.d.ts.map +1 -0
- package/build/utils/constants.js +12 -0
- package/build/utils/constants.js.map +1 -0
- package/ios/ExpoIap.podspec +1 -1
- package/ios/ExpoIapModule.swift +334 -334
- package/ios/expoiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/expoiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/jest.config.js +19 -14
- package/package.json +1 -1
- package/plugin/build/withIAP.js +20 -22
- package/plugin/src/withIAP.ts +34 -31
- package/plugin/src/withLocalOpenIAP.ts +3 -1
- package/src/helpers/subscription.ts +34 -21
- package/src/index.ts +50 -33
- package/src/modules/ios.ts +4 -5
- package/src/types/ExpoIapAndroid.types.ts +4 -3
- package/src/types/ExpoIapIOS.types.ts +3 -4
- package/src/useIAP.ts +73 -52
- package/src/utils/constants.ts +14 -0
- package/ios/ProductStore.swift +0 -27
- 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)$': [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
package/plugin/build/withIAP.js
CHANGED
|
@@ -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
|
-
//
|
|
45
|
-
|
|
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
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,16 @@ 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 ||
|
|
146
|
-
|
|
144
|
+
const localPath = options.localPath ||
|
|
145
|
+
'/Users/crossplatformkorea/Github/hyodotdev/openiap-apple';
|
|
146
|
+
logOnce(`🔧 [expo-iap] Enabling local OpenIAP development at: ${localPath}`);
|
|
147
147
|
result = (0, withLocalOpenIAP_1.default)(result, { localPath });
|
|
148
148
|
}
|
|
149
149
|
else {
|
|
150
150
|
// Ensure iOS Podfile is set up to resolve public CocoaPods specs
|
|
151
151
|
result = withIapIOS(result);
|
|
152
|
-
|
|
152
|
+
logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
|
|
153
153
|
}
|
|
154
|
-
// Set flag after first execution to prevent duplicate logs
|
|
155
|
-
hasLoggedPluginExecution = true;
|
|
156
154
|
return result;
|
|
157
155
|
}
|
|
158
156
|
catch (error) {
|
package/plugin/src/withIAP.ts
CHANGED
|
@@ -12,8 +12,16 @@ import withLocalOpenIAP from './withLocalOpenIAP';
|
|
|
12
12
|
|
|
13
13
|
const pkg = require('../../package.json');
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
|
|
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
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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,29 @@ export interface ExpoIapPluginOptions {
|
|
|
147
145
|
enableLocalDev?: boolean;
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
|
|
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 =
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
const localPath =
|
|
159
|
+
options.localPath ||
|
|
160
|
+
'/Users/crossplatformkorea/Github/hyodotdev/openiap-apple';
|
|
161
|
+
logOnce(
|
|
162
|
+
`🔧 [expo-iap] Enabling local OpenIAP development at: ${localPath}`,
|
|
163
|
+
);
|
|
164
|
+
result = withLocalOpenIAP(result, {localPath});
|
|
160
165
|
} else {
|
|
161
166
|
// Ensure iOS Podfile is set up to resolve public CocoaPods specs
|
|
162
167
|
result = withIapIOS(result);
|
|
163
|
-
|
|
168
|
+
logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
|
|
164
169
|
}
|
|
165
|
-
|
|
166
|
-
// Set flag after first execution to prevent duplicate logs
|
|
167
|
-
hasLoggedPluginExecution = true;
|
|
170
|
+
|
|
168
171
|
return result;
|
|
169
172
|
} catch (error) {
|
|
170
173
|
WarningAggregator.addWarningAndroid(
|
|
@@ -23,7 +23,9 @@ const withLocalOpenIAP: ConfigPlugin<{localPath?: string} | void> = (
|
|
|
23
23
|
|
|
24
24
|
// Check if local path exists
|
|
25
25
|
if (!fs.existsSync(localOpenIapPath)) {
|
|
26
|
-
console.warn(
|
|
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
|
);
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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,17 @@ 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 =
|
|
40
|
+
const hasSubscriptionFields =
|
|
38
41
|
('expirationDateIOS' in purchase && purchase.expirationDateIOS) ||
|
|
39
|
-
|
|
42
|
+
'autoRenewingAndroid' in purchase ||
|
|
40
43
|
('environmentIOS' in purchase && purchase.environmentIOS === 'Sandbox');
|
|
41
|
-
|
|
44
|
+
|
|
42
45
|
if (!hasSubscriptionFields) {
|
|
43
46
|
return false;
|
|
44
47
|
}
|
|
45
|
-
|
|
48
|
+
|
|
46
49
|
// Check if it's actually active
|
|
47
50
|
if (Platform.OS === 'ios') {
|
|
48
51
|
if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
|
|
@@ -53,8 +56,15 @@ export const getActiveSubscriptions = async (
|
|
|
53
56
|
if ('environmentIOS' in purchase && purchase.environmentIOS) {
|
|
54
57
|
const dayInMs = 24 * 60 * 60 * 1000;
|
|
55
58
|
// If no expiration date, consider active if transaction is recent (within 24 hours for Sandbox)
|
|
56
|
-
if (
|
|
57
|
-
|
|
59
|
+
if (
|
|
60
|
+
!('expirationDateIOS' in purchase) ||
|
|
61
|
+
!purchase.expirationDateIOS
|
|
62
|
+
) {
|
|
63
|
+
if (
|
|
64
|
+
purchase.environmentIOS === 'Sandbox' &&
|
|
65
|
+
purchase.transactionDate &&
|
|
66
|
+
currentTime - purchase.transactionDate < dayInMs
|
|
67
|
+
) {
|
|
58
68
|
return true;
|
|
59
69
|
}
|
|
60
70
|
}
|
|
@@ -63,31 +73,34 @@ export const getActiveSubscriptions = async (
|
|
|
63
73
|
// For Android, if it's in the purchases list, it's active
|
|
64
74
|
return true;
|
|
65
75
|
}
|
|
66
|
-
|
|
76
|
+
|
|
67
77
|
return false;
|
|
68
78
|
});
|
|
69
|
-
|
|
79
|
+
|
|
70
80
|
// Convert to ActiveSubscription format
|
|
71
81
|
for (const purchase of filteredPurchases) {
|
|
72
82
|
const subscription: ActiveSubscription = {
|
|
73
83
|
productId: purchase.productId,
|
|
74
84
|
isActive: true,
|
|
85
|
+
transactionId: purchase.transactionId || purchase.id,
|
|
86
|
+
purchaseToken: purchase.purchaseToken,
|
|
87
|
+
transactionDate: purchase.transactionDate,
|
|
75
88
|
};
|
|
76
|
-
|
|
89
|
+
|
|
77
90
|
// Add platform-specific details
|
|
78
91
|
if (Platform.OS === 'ios') {
|
|
79
92
|
if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
|
|
80
93
|
const expirationDate = new Date(purchase.expirationDateIOS);
|
|
81
94
|
subscription.expirationDateIOS = expirationDate;
|
|
82
|
-
|
|
95
|
+
|
|
83
96
|
// Calculate days until expiration (round to nearest day)
|
|
84
97
|
const daysUntilExpiration = Math.round(
|
|
85
|
-
(purchase.expirationDateIOS - currentTime) / (1000 * 60 * 60 * 24)
|
|
98
|
+
(purchase.expirationDateIOS - currentTime) / (1000 * 60 * 60 * 24),
|
|
86
99
|
);
|
|
87
100
|
subscription.daysUntilExpirationIOS = daysUntilExpiration;
|
|
88
101
|
subscription.willExpireSoon = daysUntilExpiration <= 7;
|
|
89
102
|
}
|
|
90
|
-
|
|
103
|
+
|
|
91
104
|
if ('environmentIOS' in purchase) {
|
|
92
105
|
subscription.environmentIOS = purchase.environmentIOS;
|
|
93
106
|
}
|
|
@@ -98,10 +111,10 @@ export const getActiveSubscriptions = async (
|
|
|
98
111
|
subscription.willExpireSoon = !purchase.autoRenewingAndroid;
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
|
-
|
|
114
|
+
|
|
102
115
|
activeSubscriptions.push(subscription);
|
|
103
116
|
}
|
|
104
|
-
|
|
117
|
+
|
|
105
118
|
return activeSubscriptions;
|
|
106
119
|
} catch (error) {
|
|
107
120
|
console.error('Error getting active subscriptions:', error);
|
|
@@ -115,8 +128,8 @@ export const getActiveSubscriptions = async (
|
|
|
115
128
|
* @returns Promise<boolean> true if user has at least one active subscription
|
|
116
129
|
*/
|
|
117
130
|
export const hasActiveSubscriptions = async (
|
|
118
|
-
subscriptionIds?: string[]
|
|
131
|
+
subscriptionIds?: string[],
|
|
119
132
|
): Promise<boolean> => {
|
|
120
133
|
const subscriptions = await getActiveSubscriptions(subscriptionIds);
|
|
121
134
|
return subscriptions.length > 0;
|
|
122
|
-
};
|
|
135
|
+
};
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
239
|
-
|
|
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({
|
|
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:
|
|
330
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
package/src/modules/ios.ts
CHANGED
|
@@ -170,15 +170,14 @@ export const beginRefundRequestIOS = (
|
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* Shows the system UI for managing subscriptions.
|
|
173
|
-
*
|
|
174
|
-
* purchaseUpdatedListener and transactionUpdatedIOS listeners.
|
|
173
|
+
* Returns an array of subscriptions that had status changes after the UI is closed.
|
|
175
174
|
*
|
|
176
|
-
* @returns Promise
|
|
175
|
+
* @returns Promise<Purchase[]> - Array of subscriptions with status changes (e.g., auto-renewal toggled)
|
|
177
176
|
* @throws Error if called on non-iOS platform
|
|
178
177
|
*
|
|
179
178
|
* @platform iOS
|
|
180
179
|
*/
|
|
181
|
-
export const showManageSubscriptionsIOS = (): Promise<
|
|
180
|
+
export const showManageSubscriptionsIOS = (): Promise<Purchase[]> => {
|
|
182
181
|
return ExpoIapModule.showManageSubscriptionsIOS();
|
|
183
182
|
};
|
|
184
183
|
|
|
@@ -417,7 +416,7 @@ export const beginRefundRequest = (
|
|
|
417
416
|
/**
|
|
418
417
|
* @deprecated Use `showManageSubscriptionsIOS` instead. This function will be removed in version 3.0.0.
|
|
419
418
|
*/
|
|
420
|
-
export const showManageSubscriptions = (): Promise<
|
|
419
|
+
export const showManageSubscriptions = (): Promise<Purchase[]> => {
|
|
421
420
|
console.warn(
|
|
422
421
|
'`showManageSubscriptions` is deprecated. Use `showManageSubscriptionsIOS` instead. This function will be removed in version 3.0.0.',
|
|
423
422
|
);
|
|
@@ -31,7 +31,7 @@ type ProductSubscriptionAndroidOfferDetail = {
|
|
|
31
31
|
export type ProductAndroid = ProductCommon & {
|
|
32
32
|
nameAndroid: string;
|
|
33
33
|
oneTimePurchaseOfferDetailsAndroid?: ProductAndroidOneTimePurchaseOfferDetail;
|
|
34
|
-
platform:
|
|
34
|
+
platform: 'android';
|
|
35
35
|
subscriptionOfferDetailsAndroid?: ProductSubscriptionAndroidOfferDetail[];
|
|
36
36
|
/**
|
|
37
37
|
* @deprecated Use `nameAndroid` instead. This field will be removed in v2.9.0.
|
|
@@ -144,7 +144,7 @@ export const PurchaseStateAndroid = PurchaseAndroidState;
|
|
|
144
144
|
|
|
145
145
|
// Legacy naming for backward compatibility
|
|
146
146
|
export type ProductPurchaseAndroid = PurchaseCommon & {
|
|
147
|
-
platform:
|
|
147
|
+
platform: 'android';
|
|
148
148
|
/**
|
|
149
149
|
* @deprecated Use `purchaseToken` instead. This field will be removed in a future version.
|
|
150
150
|
*/
|
|
@@ -167,7 +167,8 @@ export type PurchaseAndroid = ProductPurchaseAndroid;
|
|
|
167
167
|
/**
|
|
168
168
|
* @deprecated Use `ProductAndroidOneTimePurchaseOfferDetail` instead. This type will be removed in v2.9.0.
|
|
169
169
|
*/
|
|
170
|
-
export type OneTimePurchaseOfferDetails =
|
|
170
|
+
export type OneTimePurchaseOfferDetails =
|
|
171
|
+
ProductAndroidOneTimePurchaseOfferDetail;
|
|
171
172
|
|
|
172
173
|
/**
|
|
173
174
|
* @deprecated Use `ProductSubscriptionAndroidOfferDetail` instead. This type will be removed in v2.9.0.
|