expo-iap 2.9.7 → 3.0.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 +30 -0
- package/README.md +1 -1
- package/android/build.gradle +7 -2
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +195 -650
- package/android/src/main/java/expo/modules/iap/PromiseUtils.kt +85 -0
- package/build/ExpoIap.types.d.ts +0 -6
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js.map +1 -1
- package/build/helpers/subscription.d.ts.map +1 -1
- package/build/helpers/subscription.js +14 -3
- package/build/helpers/subscription.js.map +1 -1
- package/build/index.d.ts +6 -73
- package/build/index.d.ts.map +1 -1
- package/build/index.js +21 -154
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +2 -2
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +11 -1
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +0 -60
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +2 -121
- package/build/modules/ios.js.map +1 -1
- package/build/types/ExpoIapAndroid.types.d.ts +0 -8
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
- package/build/types/ExpoIapAndroid.types.js +0 -1
- package/build/types/ExpoIapAndroid.types.js.map +1 -1
- package/build/types/ExpoIapIOS.types.d.ts +0 -5
- package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
- package/build/types/ExpoIapIOS.types.js.map +1 -1
- package/build/useIAP.d.ts +0 -18
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +1 -18
- package/build/useIAP.js.map +1 -1
- package/bun.lock +340 -137
- package/codecov.yml +17 -21
- package/ios/ExpoIapModule.swift +10 -3
- package/jest.config.js +5 -9
- package/package.json +5 -3
- package/plugin/build/withIAP.d.ts +4 -1
- package/plugin/build/withIAP.js +38 -24
- package/plugin/build/withLocalOpenIAP.d.ts +6 -2
- package/plugin/build/withLocalOpenIAP.js +175 -20
- package/plugin/src/withIAP.ts +66 -30
- package/plugin/src/withLocalOpenIAP.ts +228 -24
- package/src/ExpoIap.types.ts +0 -8
- package/src/helpers/subscription.ts +14 -3
- package/src/index.ts +22 -230
- package/src/modules/android.ts +16 -6
- package/src/modules/ios.ts +2 -168
- package/src/types/ExpoIapAndroid.types.ts +0 -11
- package/src/types/ExpoIapIOS.types.ts +0 -5
- package/src/useIAP.ts +3 -55
- package/android/src/main/java/expo/modules/iap/PlayUtils.kt +0 -178
- package/android/src/main/java/expo/modules/iap/Types.kt +0 -98
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ConfigPlugin,
|
|
3
|
+
withDangerousMod,
|
|
4
|
+
withSettingsGradle,
|
|
5
|
+
withAppBuildGradle,
|
|
6
|
+
} from 'expo/config-plugins';
|
|
2
7
|
import * as fs from 'fs';
|
|
3
8
|
import * as path from 'path';
|
|
4
9
|
|
|
@@ -6,46 +11,60 @@ import * as path from 'path';
|
|
|
6
11
|
* Plugin to add local OpenIAP pod dependency for development
|
|
7
12
|
* This is only for local development with openiap-apple library
|
|
8
13
|
*/
|
|
9
|
-
|
|
14
|
+
type LocalPathOption = string | {ios?: string; android?: string};
|
|
15
|
+
|
|
16
|
+
const withLocalOpenIAP: ConfigPlugin<{localPath?: LocalPathOption} | void> = (
|
|
10
17
|
config,
|
|
11
18
|
props,
|
|
12
19
|
) => {
|
|
13
|
-
|
|
20
|
+
// Helper to resolve Android module path
|
|
21
|
+
const resolveAndroidModulePath = (p?: string): string | null => {
|
|
22
|
+
if (!p) return null;
|
|
23
|
+
// Prefer the module directory if it exists
|
|
24
|
+
const candidates = [
|
|
25
|
+
path.join(p, 'openiap-google'),
|
|
26
|
+
path.join(p, 'openiap'),
|
|
27
|
+
p,
|
|
28
|
+
];
|
|
29
|
+
for (const c of candidates) {
|
|
30
|
+
if (
|
|
31
|
+
fs.existsSync(path.join(c, 'build.gradle')) ||
|
|
32
|
+
fs.existsSync(path.join(c, 'build.gradle.kts'))
|
|
33
|
+
) {
|
|
34
|
+
return c;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// iOS: inject local pod path
|
|
41
|
+
config = withDangerousMod(config, [
|
|
14
42
|
'ios',
|
|
15
43
|
async (config) => {
|
|
16
|
-
const {platformProjectRoot} = config.modRequest;
|
|
44
|
+
const {platformProjectRoot, projectRoot} = config.modRequest as any;
|
|
45
|
+
const raw = props?.localPath;
|
|
46
|
+
const iosPath =
|
|
47
|
+
(typeof raw === 'string' ? raw : raw?.ios) ||
|
|
48
|
+
path.resolve(projectRoot, 'openiap-apple');
|
|
17
49
|
const podfilePath = path.join(platformProjectRoot, 'Podfile');
|
|
18
50
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
path.resolve(config.modRequest.projectRoot, 'openiap-apple');
|
|
23
|
-
|
|
24
|
-
// Check if local path exists
|
|
25
|
-
if (!fs.existsSync(localOpenIapPath)) {
|
|
26
|
-
console.warn(
|
|
27
|
-
`⚠️ Local openiap-apple path not found: ${localOpenIapPath}`,
|
|
28
|
-
);
|
|
29
|
-
console.warn(
|
|
30
|
-
' Skipping local pod injection. Using default pod resolution.',
|
|
31
|
-
);
|
|
51
|
+
if (!fs.existsSync(iosPath)) {
|
|
52
|
+
console.warn(`⚠️ Local openiap-apple path not found: ${iosPath}`);
|
|
53
|
+
console.warn(' Skipping local pod injection.');
|
|
32
54
|
return config;
|
|
33
55
|
}
|
|
34
56
|
|
|
35
|
-
// Read Podfile
|
|
36
57
|
if (!fs.existsSync(podfilePath)) {
|
|
37
58
|
console.warn(`⚠️ Podfile not found at ${podfilePath}. Skipping.`);
|
|
38
59
|
return config;
|
|
39
60
|
}
|
|
40
61
|
let podfileContent = fs.readFileSync(podfilePath, 'utf8');
|
|
41
62
|
|
|
42
|
-
// Check if already has the local pod reference
|
|
43
63
|
if (podfileContent.includes("pod 'openiap',")) {
|
|
44
64
|
console.log('✅ Local OpenIAP pod already configured');
|
|
45
65
|
return config;
|
|
46
66
|
}
|
|
47
67
|
|
|
48
|
-
// Find the target block and inject the local pod
|
|
49
68
|
const targetRegex =
|
|
50
69
|
/target\s+['"][\w]+['"]\s+do\s*\n\s*use_expo_modules!/;
|
|
51
70
|
|
|
@@ -54,12 +73,10 @@ const withLocalOpenIAP: ConfigPlugin<{localPath?: string} | void> = (
|
|
|
54
73
|
return `${match}
|
|
55
74
|
|
|
56
75
|
# Local OpenIAP pod for development (added by expo-iap plugin)
|
|
57
|
-
pod 'openiap', :path => '${
|
|
76
|
+
pod 'openiap', :path => '${iosPath}'`;
|
|
58
77
|
});
|
|
59
|
-
|
|
60
|
-
// Write back to Podfile
|
|
61
78
|
fs.writeFileSync(podfilePath, podfileContent);
|
|
62
|
-
console.log(`✅ Added local OpenIAP pod at: ${
|
|
79
|
+
console.log(`✅ Added local OpenIAP pod at: ${iosPath}`);
|
|
63
80
|
} else {
|
|
64
81
|
console.warn('⚠️ Could not find target block in Podfile');
|
|
65
82
|
}
|
|
@@ -67,6 +84,193 @@ const withLocalOpenIAP: ConfigPlugin<{localPath?: string} | void> = (
|
|
|
67
84
|
return config;
|
|
68
85
|
},
|
|
69
86
|
]);
|
|
87
|
+
|
|
88
|
+
// Android: include local module and add dependency if available
|
|
89
|
+
config = withSettingsGradle(config, (config) => {
|
|
90
|
+
const raw = props?.localPath;
|
|
91
|
+
const projectRoot = (config.modRequest as any).projectRoot as string;
|
|
92
|
+
const androidInput = typeof raw === 'string' ? undefined : raw?.android;
|
|
93
|
+
const androidModulePath =
|
|
94
|
+
resolveAndroidModulePath(androidInput) ||
|
|
95
|
+
resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) ||
|
|
96
|
+
null;
|
|
97
|
+
|
|
98
|
+
if (!androidModulePath || !fs.existsSync(androidModulePath)) {
|
|
99
|
+
if (androidInput) {
|
|
100
|
+
console.warn(
|
|
101
|
+
`⚠️ Could not resolve Android OpenIAP module at: ${androidInput}. Skipping local Android linkage.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 1) settings.gradle: include and map projectDir
|
|
108
|
+
const settings = config.modResults;
|
|
109
|
+
const includeLine = "include ':openiap-google'";
|
|
110
|
+
const projectDirLine = `project(':openiap-google').projectDir = new File('${androidModulePath.replace(
|
|
111
|
+
/\\/g,
|
|
112
|
+
'/',
|
|
113
|
+
)}')`;
|
|
114
|
+
let contents = settings.contents ?? '';
|
|
115
|
+
|
|
116
|
+
// Ensure pluginManagement has plugin mappings required by the included module
|
|
117
|
+
const injectPluginManagement = () => {
|
|
118
|
+
const header = 'pluginManagement {';
|
|
119
|
+
const needsVannik =
|
|
120
|
+
!/id\s*\(\s*["']com\.vanniktech\.maven\.publish["']/.test(contents);
|
|
121
|
+
const needsKotlinAndroid =
|
|
122
|
+
!/id\s*\(\s*["']org\.jetbrains\.kotlin\.android["']/.test(contents);
|
|
123
|
+
const needsCompose =
|
|
124
|
+
!/id\s*\(\s*["']org\.jetbrains\.kotlin\.plugin\.compose["']/.test(
|
|
125
|
+
contents,
|
|
126
|
+
);
|
|
127
|
+
const needsRepos = !/pluginManagement[\s\S]*?repositories\s*\{/.test(
|
|
128
|
+
contents,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const pluginLines: string[] = [];
|
|
132
|
+
if (needsVannik)
|
|
133
|
+
pluginLines.push(
|
|
134
|
+
` id("com.vanniktech.maven.publish") version "0.29.0"`,
|
|
135
|
+
);
|
|
136
|
+
if (needsKotlinAndroid)
|
|
137
|
+
pluginLines.push(
|
|
138
|
+
` id("org.jetbrains.kotlin.android") version "2.0.21"`,
|
|
139
|
+
);
|
|
140
|
+
if (needsCompose)
|
|
141
|
+
pluginLines.push(
|
|
142
|
+
` id("org.jetbrains.kotlin.plugin.compose") version "2.0.21"`,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// If everything already present, skip
|
|
146
|
+
if (pluginLines.length === 0 && !needsRepos) return;
|
|
147
|
+
|
|
148
|
+
const pluginsBlock = pluginLines.length
|
|
149
|
+
? `plugins {\n${pluginLines.join('\n')}\n}`
|
|
150
|
+
: '';
|
|
151
|
+
const reposBlock = `repositories { gradlePluginPortal(); google(); mavenCentral() }`;
|
|
152
|
+
|
|
153
|
+
if (contents.includes(header)) {
|
|
154
|
+
contents = contents.replace(/pluginManagement\s*\{/, (m) => {
|
|
155
|
+
let injection =
|
|
156
|
+
m + `\n // Added by expo-iap (local openiap-google)\n`;
|
|
157
|
+
if (pluginsBlock) injection += ` ${pluginsBlock}\n`;
|
|
158
|
+
if (needsRepos) injection += ` ${reposBlock}\n`;
|
|
159
|
+
return injection;
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
contents =
|
|
163
|
+
`pluginManagement {\n // Added by expo-iap (local openiap-google)\n` +
|
|
164
|
+
(pluginsBlock ? ` ${pluginsBlock}\n` : '') +
|
|
165
|
+
` ${reposBlock}\n}\n\n${contents}`;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
!/com\.vanniktech\.maven\.publish/.test(contents) ||
|
|
171
|
+
!/org\.jetbrains\.kotlin\.android/.test(contents)
|
|
172
|
+
) {
|
|
173
|
+
injectPluginManagement();
|
|
174
|
+
}
|
|
175
|
+
if (!contents.includes(includeLine)) contents += `\n${includeLine}\n`;
|
|
176
|
+
if (!contents.includes(projectDirLine)) contents += `${projectDirLine}\n`;
|
|
177
|
+
settings.contents = contents;
|
|
178
|
+
console.log(`✅ Linked local Android module at: ${androidModulePath}`);
|
|
179
|
+
return config;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 2) app/build.gradle: add implementation project(':openiap-google')
|
|
183
|
+
config = withAppBuildGradle(config, (config) => {
|
|
184
|
+
const raw = props?.localPath;
|
|
185
|
+
const projectRoot = (config.modRequest as any).projectRoot as string;
|
|
186
|
+
const androidInput = typeof raw === 'string' ? undefined : raw?.android;
|
|
187
|
+
const androidModulePath =
|
|
188
|
+
resolveAndroidModulePath(androidInput) ||
|
|
189
|
+
resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) ||
|
|
190
|
+
null;
|
|
191
|
+
|
|
192
|
+
if (!androidModulePath || !fs.existsSync(androidModulePath)) {
|
|
193
|
+
return config;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const gradle = config.modResults;
|
|
197
|
+
const dependencyLine = ` implementation project(':openiap-google')`;
|
|
198
|
+
|
|
199
|
+
// Remove any previously injected external deps that can conflict with the local module
|
|
200
|
+
const removalPatterns = [
|
|
201
|
+
/\n\s*implementation\s+"com\.android\.billingclient:billing-ktx:[^"]+"\s*\n/g,
|
|
202
|
+
/\n\s*implementation\s+"com\.google\.android\.gms:play-services-base:[^"]+"\s*\n/g,
|
|
203
|
+
];
|
|
204
|
+
let contents = gradle.contents;
|
|
205
|
+
let removedAny = false;
|
|
206
|
+
for (const pattern of removalPatterns) {
|
|
207
|
+
if (pattern.test(contents)) {
|
|
208
|
+
contents = contents.replace(pattern, '\n');
|
|
209
|
+
removedAny = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (removedAny) {
|
|
213
|
+
gradle.contents = contents;
|
|
214
|
+
console.log(
|
|
215
|
+
'🧹 Removed external Play Billing/GMS deps to use local :openiap-google',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (!gradle.contents.includes(dependencyLine)) {
|
|
219
|
+
const anchor = /dependencies\s*\{/m;
|
|
220
|
+
if (anchor.test(gradle.contents)) {
|
|
221
|
+
gradle.contents = gradle.contents.replace(
|
|
222
|
+
anchor,
|
|
223
|
+
(m) => `${m}\n${dependencyLine}`,
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
gradle.contents += `\n\ndependencies {\n${dependencyLine}\n}\n`;
|
|
227
|
+
}
|
|
228
|
+
console.log('🛠️ Added dependency on local :openiap-google project');
|
|
229
|
+
}
|
|
230
|
+
return config;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// 3) Ensure final cleanup in app/build.gradle after all mods are applied
|
|
234
|
+
config = withDangerousMod(config, [
|
|
235
|
+
'android',
|
|
236
|
+
async (config) => {
|
|
237
|
+
try {
|
|
238
|
+
const {platformProjectRoot} = config.modRequest as any;
|
|
239
|
+
const appBuildGradle = path.join(
|
|
240
|
+
platformProjectRoot,
|
|
241
|
+
'app',
|
|
242
|
+
'build.gradle',
|
|
243
|
+
);
|
|
244
|
+
if (fs.existsSync(appBuildGradle)) {
|
|
245
|
+
let contents = fs.readFileSync(appBuildGradle, 'utf8');
|
|
246
|
+
const patterns = [
|
|
247
|
+
/\n\s*implementation\s+"com\.android\.billingclient:billing-ktx:[^"]+"\s*\n/g,
|
|
248
|
+
/\n\s*implementation\s+"com\.google\.android\.gms:play-services-base:[^"]+"\s*\n/g,
|
|
249
|
+
];
|
|
250
|
+
let changed = false;
|
|
251
|
+
for (const p of patterns) {
|
|
252
|
+
if (p.test(contents)) {
|
|
253
|
+
contents = contents.replace(p, '\n');
|
|
254
|
+
changed = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (changed) {
|
|
258
|
+
fs.writeFileSync(appBuildGradle, contents);
|
|
259
|
+
console.log(
|
|
260
|
+
'🧹 expo-iap: Cleaned app/build.gradle billing/gms deps for local :openiap-google',
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch (e) {
|
|
265
|
+
console.warn('expo-iap: cleanup step failed:', e);
|
|
266
|
+
}
|
|
267
|
+
return config;
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
// (removed) Avoid global root build.gradle mutations; included module should manage its plugins
|
|
272
|
+
|
|
273
|
+
return config;
|
|
70
274
|
};
|
|
71
275
|
|
|
72
276
|
export default withLocalOpenIAP;
|
package/src/ExpoIap.types.ts
CHANGED
|
@@ -37,7 +37,6 @@ export type PurchaseCommon = {
|
|
|
37
37
|
id: string; // Transaction identifier - used by finishTransaction
|
|
38
38
|
productId: string; // Product identifier - which product was purchased
|
|
39
39
|
ids?: string[]; // Product identifiers for purchases that include multiple products
|
|
40
|
-
transactionId?: string; // @deprecated - use id instead
|
|
41
40
|
transactionDate: number;
|
|
42
41
|
transactionReceipt: string;
|
|
43
42
|
purchaseToken?: string; // Unified purchase token (jwsRepresentation for iOS, purchaseToken for Android)
|
|
@@ -70,17 +69,11 @@ export type Purchase =
|
|
|
70
69
|
| (PurchaseAndroid & AndroidPlatform)
|
|
71
70
|
| (PurchaseIOS & IosPlatform);
|
|
72
71
|
|
|
73
|
-
// Removed legacy type aliases `ProductPurchase` and `SubscriptionPurchase` in v2.9.0
|
|
74
|
-
|
|
75
72
|
export type PurchaseResult = {
|
|
76
73
|
responseCode?: number;
|
|
77
74
|
debugMessage?: string;
|
|
78
75
|
code?: string;
|
|
79
76
|
message?: string;
|
|
80
|
-
/**
|
|
81
|
-
* @deprecated Use `purchaseToken` instead. This field will be removed in a future version.
|
|
82
|
-
*/
|
|
83
|
-
purchaseTokenAndroid?: string;
|
|
84
77
|
purchaseToken?: string;
|
|
85
78
|
};
|
|
86
79
|
/**
|
|
@@ -399,7 +392,6 @@ export interface RequestPurchaseAndroidProps {
|
|
|
399
392
|
*/
|
|
400
393
|
export interface RequestSubscriptionAndroidProps
|
|
401
394
|
extends RequestPurchaseAndroidProps {
|
|
402
|
-
readonly purchaseTokenAndroid?: string;
|
|
403
395
|
readonly replacementModeAndroid?: number;
|
|
404
396
|
readonly subscriptionOffers: {
|
|
405
397
|
sku: string;
|
|
@@ -39,7 +39,8 @@ export const getActiveSubscriptions = async (
|
|
|
39
39
|
// Check if this purchase has subscription-specific fields
|
|
40
40
|
const hasSubscriptionFields =
|
|
41
41
|
('expirationDateIOS' in purchase && !!purchase.expirationDateIOS) ||
|
|
42
|
-
'autoRenewingAndroid' in purchase
|
|
42
|
+
'autoRenewingAndroid' in purchase ||
|
|
43
|
+
('environmentIOS' in purchase && !!(purchase as any).environmentIOS);
|
|
43
44
|
|
|
44
45
|
if (!hasSubscriptionFields) {
|
|
45
46
|
return false;
|
|
@@ -76,12 +77,22 @@ export const getActiveSubscriptions = async (
|
|
|
76
77
|
return false;
|
|
77
78
|
});
|
|
78
79
|
|
|
80
|
+
// Deduplicate by transaction identifier (id)
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
const dedupedPurchases = filteredPurchases.filter((p) => {
|
|
83
|
+
const key = String(p.id);
|
|
84
|
+
if (seen.has(key)) return false;
|
|
85
|
+
seen.add(key);
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
|
|
79
89
|
// Convert to ActiveSubscription format
|
|
80
|
-
for (const purchase of
|
|
90
|
+
for (const purchase of dedupedPurchases) {
|
|
81
91
|
const subscription: ActiveSubscription = {
|
|
82
92
|
productId: purchase.productId,
|
|
83
93
|
isActive: true,
|
|
84
|
-
|
|
94
|
+
// Use unified id as transaction identifier in v3
|
|
95
|
+
transactionId: purchase.id,
|
|
85
96
|
purchaseToken: purchase.purchaseToken,
|
|
86
97
|
transactionDate: purchase.transactionDate,
|
|
87
98
|
};
|
package/src/index.ts
CHANGED
|
@@ -49,8 +49,6 @@ export const PI = ExpoIapModule.PI;
|
|
|
49
49
|
export enum OpenIapEvent {
|
|
50
50
|
PurchaseUpdated = 'purchase-updated',
|
|
51
51
|
PurchaseError = 'purchase-error',
|
|
52
|
-
/** @deprecated Use PurchaseUpdated instead. This will be removed in a future version. */
|
|
53
|
-
TransactionIapUpdated = 'iap-transaction-updated',
|
|
54
52
|
PromotedProductIOS = 'promoted-product-ios',
|
|
55
53
|
}
|
|
56
54
|
|
|
@@ -139,89 +137,6 @@ export function initConnection(): Promise<boolean> {
|
|
|
139
137
|
return Promise.resolve(result);
|
|
140
138
|
}
|
|
141
139
|
|
|
142
|
-
export const getProducts = async (skus: string[]): Promise<Product[]> => {
|
|
143
|
-
console.warn(
|
|
144
|
-
"`getProducts` is deprecated. Use `fetchProducts({ skus, type: 'inapp' })` instead. This function will be removed in version 3.0.0.",
|
|
145
|
-
);
|
|
146
|
-
if (!skus?.length) {
|
|
147
|
-
return Promise.reject(
|
|
148
|
-
new PurchaseError({
|
|
149
|
-
message: 'No SKUs provided',
|
|
150
|
-
code: ErrorCode.E_EMPTY_SKU_LIST,
|
|
151
|
-
}),
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return Platform.select({
|
|
156
|
-
ios: async () => {
|
|
157
|
-
const rawItems = await ExpoIapModule.fetchProducts(skus);
|
|
158
|
-
return rawItems.filter((item: unknown) => {
|
|
159
|
-
if (!isProductIOS(item)) return false;
|
|
160
|
-
return (
|
|
161
|
-
typeof item === 'object' &&
|
|
162
|
-
item !== null &&
|
|
163
|
-
'id' in item &&
|
|
164
|
-
typeof item.id === 'string' &&
|
|
165
|
-
skus.includes(item.id)
|
|
166
|
-
);
|
|
167
|
-
}) as Product[];
|
|
168
|
-
},
|
|
169
|
-
android: async () => {
|
|
170
|
-
const products = await ExpoIapModule.fetchProducts('inapp', skus);
|
|
171
|
-
return products.filter((product: unknown) =>
|
|
172
|
-
isProductAndroid<Product>(product),
|
|
173
|
-
);
|
|
174
|
-
},
|
|
175
|
-
default: () => Promise.reject(new Error('Unsupported Platform')),
|
|
176
|
-
})();
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
export const getSubscriptions = async (
|
|
180
|
-
skus: string[],
|
|
181
|
-
): Promise<SubscriptionProduct[]> => {
|
|
182
|
-
console.warn(
|
|
183
|
-
"`getSubscriptions` is deprecated. Use `fetchProducts({ skus, type: 'subs' })` instead. This function will be removed in version 3.0.0.",
|
|
184
|
-
);
|
|
185
|
-
if (!skus?.length) {
|
|
186
|
-
return Promise.reject(
|
|
187
|
-
new PurchaseError({
|
|
188
|
-
message: 'No SKUs provided',
|
|
189
|
-
code: ErrorCode.E_EMPTY_SKU_LIST,
|
|
190
|
-
}),
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return Platform.select({
|
|
195
|
-
ios: async () => {
|
|
196
|
-
const rawItems = await ExpoIapModule.fetchProducts(skus);
|
|
197
|
-
return rawItems.filter((item: unknown) => {
|
|
198
|
-
if (!isProductIOS(item)) return false;
|
|
199
|
-
return (
|
|
200
|
-
typeof item === 'object' &&
|
|
201
|
-
item !== null &&
|
|
202
|
-
'id' in item &&
|
|
203
|
-
typeof item.id === 'string' &&
|
|
204
|
-
skus.includes(item.id)
|
|
205
|
-
);
|
|
206
|
-
}) as SubscriptionProduct[];
|
|
207
|
-
},
|
|
208
|
-
android: async () => {
|
|
209
|
-
const rawItems = await ExpoIapModule.fetchProducts('subs', skus);
|
|
210
|
-
return rawItems.filter((item: unknown) => {
|
|
211
|
-
if (!isProductAndroid(item)) return false;
|
|
212
|
-
return (
|
|
213
|
-
typeof item === 'object' &&
|
|
214
|
-
item !== null &&
|
|
215
|
-
'id' in item &&
|
|
216
|
-
typeof item.id === 'string' &&
|
|
217
|
-
skus.includes(item.id)
|
|
218
|
-
);
|
|
219
|
-
}) as SubscriptionProduct[];
|
|
220
|
-
},
|
|
221
|
-
default: () => Promise.reject(new Error('Unsupported Platform')),
|
|
222
|
-
})();
|
|
223
|
-
};
|
|
224
|
-
|
|
225
140
|
export async function endConnection(): Promise<boolean> {
|
|
226
141
|
return ExpoIapModule.endConnection();
|
|
227
142
|
}
|
|
@@ -304,81 +219,10 @@ export const fetchProducts = async ({
|
|
|
304
219
|
throw new Error('Unsupported platform');
|
|
305
220
|
};
|
|
306
221
|
|
|
307
|
-
/**
|
|
308
|
-
* @deprecated Use `fetchProducts` instead. This method will be removed in version 3.0.0.
|
|
309
|
-
*
|
|
310
|
-
* The 'request' prefix should only be used for event-based operations that trigger
|
|
311
|
-
* purchase flows. Since this function simply fetches product information, it has been
|
|
312
|
-
* renamed to `fetchProducts` to follow OpenIAP terminology guidelines.
|
|
313
|
-
*
|
|
314
|
-
* @example
|
|
315
|
-
* ```typescript
|
|
316
|
-
* // Old way (deprecated)
|
|
317
|
-
* const products = await requestProducts({
|
|
318
|
-
* skus: ['com.example.product1'],
|
|
319
|
-
* type: 'inapp'
|
|
320
|
-
* });
|
|
321
|
-
*
|
|
322
|
-
* // New way (recommended)
|
|
323
|
-
* const products = await fetchProducts({
|
|
324
|
-
* skus: ['com.example.product1'],
|
|
325
|
-
* type: 'inapp'
|
|
326
|
-
* });
|
|
327
|
-
* ```
|
|
328
|
-
*/
|
|
329
|
-
export const requestProducts = async ({
|
|
330
|
-
skus,
|
|
331
|
-
type = 'inapp',
|
|
332
|
-
}: {
|
|
333
|
-
skus: string[];
|
|
334
|
-
type?: 'inapp' | 'subs';
|
|
335
|
-
}): Promise<Product[] | SubscriptionProduct[]> => {
|
|
336
|
-
console.warn(
|
|
337
|
-
"`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.",
|
|
338
|
-
);
|
|
339
|
-
return fetchProducts({skus, type});
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* @deprecated Use `getPurchaseHistories` instead. This function will be removed in version 3.0.0.
|
|
344
|
-
*/
|
|
345
|
-
export const getPurchaseHistory = ({
|
|
346
|
-
alsoPublishToEventListener = false,
|
|
347
|
-
onlyIncludeActiveItems = false,
|
|
348
|
-
alsoPublishToEventListenerIOS,
|
|
349
|
-
onlyIncludeActiveItemsIOS,
|
|
350
|
-
}: {
|
|
351
|
-
/** @deprecated Use alsoPublishToEventListenerIOS instead */
|
|
352
|
-
alsoPublishToEventListener?: boolean;
|
|
353
|
-
/** @deprecated Use onlyIncludeActiveItemsIOS instead */
|
|
354
|
-
onlyIncludeActiveItems?: boolean;
|
|
355
|
-
alsoPublishToEventListenerIOS?: boolean;
|
|
356
|
-
onlyIncludeActiveItemsIOS?: boolean;
|
|
357
|
-
} = {}): Promise<Purchase[]> => {
|
|
358
|
-
console.warn(
|
|
359
|
-
'`getPurchaseHistory` is deprecated. Use `getPurchaseHistories` instead. This function will be removed in version 3.0.0.',
|
|
360
|
-
);
|
|
361
|
-
// Use available purchases as a best-effort replacement
|
|
362
|
-
return getAvailablePurchases({
|
|
363
|
-
alsoPublishToEventListenerIOS:
|
|
364
|
-
alsoPublishToEventListenerIOS ?? alsoPublishToEventListener,
|
|
365
|
-
onlyIncludeActiveItemsIOS:
|
|
366
|
-
onlyIncludeActiveItemsIOS ?? onlyIncludeActiveItems,
|
|
367
|
-
});
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
// NOTE: `getPurchaseHistories` removed in v2.9.0. Use `getAvailablePurchases` instead.
|
|
371
|
-
|
|
372
222
|
export const getAvailablePurchases = ({
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
alsoPublishToEventListenerIOS,
|
|
376
|
-
onlyIncludeActiveItemsIOS,
|
|
223
|
+
alsoPublishToEventListenerIOS = false,
|
|
224
|
+
onlyIncludeActiveItemsIOS = true,
|
|
377
225
|
}: {
|
|
378
|
-
/** @deprecated Use alsoPublishToEventListenerIOS instead */
|
|
379
|
-
alsoPublishToEventListener?: boolean;
|
|
380
|
-
/** @deprecated Use onlyIncludeActiveItemsIOS instead */
|
|
381
|
-
onlyIncludeActiveItems?: boolean;
|
|
382
226
|
alsoPublishToEventListenerIOS?: boolean;
|
|
383
227
|
onlyIncludeActiveItemsIOS?: boolean;
|
|
384
228
|
} = {}): Promise<Purchase[]> =>
|
|
@@ -386,16 +230,10 @@ export const getAvailablePurchases = ({
|
|
|
386
230
|
Platform.select({
|
|
387
231
|
ios: () =>
|
|
388
232
|
ExpoIapModule.getAvailableItems(
|
|
389
|
-
alsoPublishToEventListenerIOS
|
|
390
|
-
onlyIncludeActiveItemsIOS
|
|
233
|
+
alsoPublishToEventListenerIOS,
|
|
234
|
+
onlyIncludeActiveItemsIOS,
|
|
391
235
|
),
|
|
392
|
-
android:
|
|
393
|
-
const products = await ExpoIapModule.getAvailableItemsByType('inapp');
|
|
394
|
-
const subscriptions = await ExpoIapModule.getAvailableItemsByType(
|
|
395
|
-
'subs',
|
|
396
|
-
);
|
|
397
|
-
return products.concat(subscriptions);
|
|
398
|
-
},
|
|
236
|
+
android: () => ExpoIapModule.getAvailableItems(),
|
|
399
237
|
}) || (() => Promise.resolve([]))
|
|
400
238
|
)();
|
|
401
239
|
|
|
@@ -574,7 +412,6 @@ export const requestPurchase = (
|
|
|
574
412
|
isOfferPersonalized,
|
|
575
413
|
subscriptionOffers = [],
|
|
576
414
|
replacementModeAndroid = -1,
|
|
577
|
-
purchaseTokenAndroid,
|
|
578
415
|
purchaseToken,
|
|
579
416
|
} = normalizedRequest;
|
|
580
417
|
|
|
@@ -582,7 +419,7 @@ export const requestPurchase = (
|
|
|
582
419
|
return ExpoIapModule.requestPurchase({
|
|
583
420
|
type: 'subs',
|
|
584
421
|
skuArr: skus,
|
|
585
|
-
purchaseToken
|
|
422
|
+
purchaseToken,
|
|
586
423
|
replacementMode: replacementModeAndroid,
|
|
587
424
|
obfuscatedAccountId: obfuscatedAccountIdAndroid,
|
|
588
425
|
obfuscatedProfileId: obfuscatedProfileIdAndroid,
|
|
@@ -600,44 +437,6 @@ export const requestPurchase = (
|
|
|
600
437
|
return Promise.resolve(); // Fallback for unsupported platforms
|
|
601
438
|
};
|
|
602
439
|
|
|
603
|
-
/**
|
|
604
|
-
* @deprecated Use `requestPurchase({ request, type: 'subs' })` instead. This method will be removed in version 3.0.0.
|
|
605
|
-
*
|
|
606
|
-
* @example
|
|
607
|
-
* ```typescript
|
|
608
|
-
* // Old way (deprecated)
|
|
609
|
-
* await requestSubscription({
|
|
610
|
-
* sku: subscriptionId,
|
|
611
|
-
* // or for Android
|
|
612
|
-
* skus: [subscriptionId],
|
|
613
|
-
* });
|
|
614
|
-
*
|
|
615
|
-
* // New way (recommended)
|
|
616
|
-
* await requestPurchase({
|
|
617
|
-
* request: {
|
|
618
|
-
* ios: { sku: subscriptionId },
|
|
619
|
-
* android: {
|
|
620
|
-
* skus: [subscriptionId],
|
|
621
|
-
* subscriptionOffers: [{ sku: subscriptionId, offerToken: 'token' }]
|
|
622
|
-
* }
|
|
623
|
-
* },
|
|
624
|
-
* type: 'subs'
|
|
625
|
-
* });
|
|
626
|
-
* ```
|
|
627
|
-
*/
|
|
628
|
-
export const requestSubscription = async (
|
|
629
|
-
request: RequestSubscriptionProps,
|
|
630
|
-
): Promise<Purchase | Purchase[] | null | void> => {
|
|
631
|
-
console.warn(
|
|
632
|
-
"`requestSubscription` is deprecated and will be removed in version 3.0.0. Use `requestPurchase({ request, type: 'subs' })` instead.",
|
|
633
|
-
);
|
|
634
|
-
return (await requestPurchase({request, type: 'subs'})) as
|
|
635
|
-
| Purchase
|
|
636
|
-
| Purchase[]
|
|
637
|
-
| null
|
|
638
|
-
| void;
|
|
639
|
-
};
|
|
640
|
-
|
|
641
440
|
export const finishTransaction = ({
|
|
642
441
|
purchase,
|
|
643
442
|
isConsumable = false,
|
|
@@ -661,8 +460,7 @@ export const finishTransaction = ({
|
|
|
661
460
|
const androidPurchase = purchase as PurchaseAndroid;
|
|
662
461
|
|
|
663
462
|
// Use purchaseToken if available, fallback to purchaseTokenAndroid for backward compatibility
|
|
664
|
-
const token =
|
|
665
|
-
androidPurchase.purchaseToken || androidPurchase.purchaseTokenAndroid;
|
|
463
|
+
const token = androidPurchase.purchaseToken;
|
|
666
464
|
|
|
667
465
|
if (!token) {
|
|
668
466
|
return Promise.reject(
|
|
@@ -676,7 +474,7 @@ export const finishTransaction = ({
|
|
|
676
474
|
}
|
|
677
475
|
|
|
678
476
|
if (isConsumable) {
|
|
679
|
-
return ExpoIapModule.
|
|
477
|
+
return ExpoIapModule.consumePurchaseAndroid(token);
|
|
680
478
|
}
|
|
681
479
|
|
|
682
480
|
return ExpoIapModule.acknowledgePurchaseAndroid(token);
|
|
@@ -708,12 +506,20 @@ export const getStorefrontIOS = (): Promise<string> => {
|
|
|
708
506
|
};
|
|
709
507
|
|
|
710
508
|
/**
|
|
711
|
-
*
|
|
509
|
+
* Gets the storefront country code from the underlying native store.
|
|
510
|
+
* Returns a two-letter country code such as 'US', 'KR', or empty string on failure.
|
|
511
|
+
*
|
|
512
|
+
* @platform ios
|
|
513
|
+
* @platform android
|
|
712
514
|
*/
|
|
713
515
|
export const getStorefront = (): Promise<string> => {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
516
|
+
// Cross-platform storefront
|
|
517
|
+
if (Platform.OS === 'android') {
|
|
518
|
+
if (typeof (ExpoIapModule as any).getStorefrontAndroid === 'function') {
|
|
519
|
+
return (ExpoIapModule as any).getStorefrontAndroid();
|
|
520
|
+
}
|
|
521
|
+
return Promise.resolve('');
|
|
522
|
+
}
|
|
717
523
|
return getStorefrontIOS();
|
|
718
524
|
};
|
|
719
525
|
|
|
@@ -786,23 +592,9 @@ export const deepLinkToSubscriptions = (options: {
|
|
|
786
592
|
}
|
|
787
593
|
|
|
788
594
|
if (Platform.OS === 'android') {
|
|
789
|
-
if (!options.skuAndroid) {
|
|
790
|
-
return Promise.reject(
|
|
791
|
-
new Error(
|
|
792
|
-
'skuAndroid is required to locate subscription in Android Store',
|
|
793
|
-
),
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
if (!options.packageNameAndroid) {
|
|
797
|
-
return Promise.reject(
|
|
798
|
-
new Error(
|
|
799
|
-
'packageNameAndroid is required to identify your app in Android Store',
|
|
800
|
-
),
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
595
|
return deepLinkToSubscriptionsAndroid({
|
|
804
|
-
sku: options
|
|
805
|
-
packageName: options
|
|
596
|
+
sku: options?.skuAndroid,
|
|
597
|
+
packageName: options?.packageNameAndroid,
|
|
806
598
|
});
|
|
807
599
|
}
|
|
808
600
|
|