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.
- 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 +10 -5
- 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 +341 -334
- package/jest.config.js +19 -14
- package/package.json +2 -3
- package/plugin/build/withIAP.js +25 -23
- package/plugin/build/withLocalOpenIAP.js +5 -1
- package/plugin/src/withIAP.ts +39 -31
- package/plugin/src/withLocalOpenIAP.ts +8 -2
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/helpers/subscription.ts +35 -23
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "2.9.0
|
|
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
|
}
|
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,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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
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',")) {
|
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,34 @@ 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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
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(
|
|
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/
|
|
1
|
+
{"root":["./src/withIAP.ts","./src/withLocalOpenIAP.ts"],"version":"5.9.2"}
|
|
@@ -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,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
|
-
|
|
40
|
-
|
|
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 (
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|