expo-iap 3.1.18 → 3.1.19
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/android/build.gradle +5 -0
- package/bun.lockb +0 -0
- package/coverage/clover.xml +2 -2
- package/coverage/lcov-report/index.html +1 -1
- package/coverage/lcov-report/src/index.html +1 -1
- package/coverage/lcov-report/src/index.ts.html +1 -1
- package/coverage/lcov-report/src/modules/android.ts.html +1 -1
- package/coverage/lcov-report/src/modules/index.html +1 -1
- package/coverage/lcov-report/src/modules/ios.ts.html +1 -1
- package/coverage/lcov-report/src/utils/debug.ts.html +1 -1
- package/coverage/lcov-report/src/utils/errorMapping.ts.html +1 -1
- package/coverage/lcov-report/src/utils/index.html +1 -1
- package/openiap-versions.json +2 -2
- package/package.json +1 -1
- package/plugin/build/withIAP.d.ts +39 -21
- package/plugin/build/withIAP.js +72 -62
- package/plugin/build/withIosAlternativeBilling.d.ts +19 -0
- package/plugin/build/withIosAlternativeBilling.js +70 -0
- package/plugin/build/withLocalOpenIAP.d.ts +4 -1
- package/plugin/build/withLocalOpenIAP.js +58 -57
- package/plugin/src/withIAP.ts +145 -123
- package/plugin/src/withIosAlternativeBilling.ts +133 -0
- package/plugin/src/withLocalOpenIAP.ts +80 -67
- package/plugin/tsconfig.tsbuildinfo +1 -1
|
@@ -36,13 +36,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
const config_plugins_1 = require("expo/config-plugins");
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const withIosAlternativeBilling_1 = require("./withIosAlternativeBilling");
|
|
40
|
+
// Log a message only once per Node process
|
|
41
|
+
const logOnce = (() => {
|
|
42
|
+
const printed = new Set();
|
|
43
|
+
return (msg) => {
|
|
44
|
+
if (!printed.has(msg)) {
|
|
45
|
+
console.log(msg);
|
|
46
|
+
printed.add(msg);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
39
50
|
const withLocalOpenIAP = (config, props) => {
|
|
40
51
|
// Import and apply iOS alternative billing configuration if provided
|
|
41
52
|
if (props?.iosAlternativeBilling) {
|
|
42
|
-
|
|
43
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
|
-
const { withIosAlternativeBilling } = require('./withIAP');
|
|
45
|
-
config = withIosAlternativeBilling(config, props.iosAlternativeBilling);
|
|
53
|
+
config = (0, withIosAlternativeBilling_1.withIosAlternativeBilling)(config, props.iosAlternativeBilling);
|
|
46
54
|
}
|
|
47
55
|
// Helper to resolve Android module path
|
|
48
56
|
const resolveAndroidModulePath = (p) => {
|
|
@@ -82,7 +90,7 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
82
90
|
}
|
|
83
91
|
let podfileContent = fs.readFileSync(podfilePath, 'utf8');
|
|
84
92
|
if (podfileContent.includes("pod 'openiap',")) {
|
|
85
|
-
|
|
93
|
+
logOnce('✅ Local OpenIAP pod already configured');
|
|
86
94
|
return config;
|
|
87
95
|
}
|
|
88
96
|
const targetRegex = /target\s+['"][\w]+['"]\s+do\s*\n\s*use_expo_modules!/;
|
|
@@ -94,7 +102,7 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
94
102
|
pod 'openiap', :path => '${iosPath}'`;
|
|
95
103
|
});
|
|
96
104
|
fs.writeFileSync(podfilePath, podfileContent);
|
|
97
|
-
|
|
105
|
+
logOnce(`✅ Added local OpenIAP pod at: ${iosPath}`);
|
|
98
106
|
}
|
|
99
107
|
else {
|
|
100
108
|
console.warn('⚠️ Could not find target block in Podfile');
|
|
@@ -168,13 +176,13 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
168
176
|
if (!contents.includes(projectDirLine))
|
|
169
177
|
contents += `${projectDirLine}\n`;
|
|
170
178
|
settings.contents = contents;
|
|
171
|
-
|
|
179
|
+
logOnce(`✅ Linked local Android module at: ${androidModulePath}`);
|
|
172
180
|
return config;
|
|
173
181
|
});
|
|
174
182
|
// 2) app/build.gradle: add implementation project(':openiap-google')
|
|
175
183
|
config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
|
|
176
|
-
const raw = props?.localPath;
|
|
177
184
|
const projectRoot = config.modRequest.projectRoot;
|
|
185
|
+
const raw = props?.localPath;
|
|
178
186
|
const androidInput = typeof raw === 'string' ? undefined : raw?.android;
|
|
179
187
|
const androidModulePath = resolveAndroidModulePath(androidInput) ||
|
|
180
188
|
resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) ||
|
|
@@ -184,72 +192,65 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
184
192
|
}
|
|
185
193
|
const gradle = config.modResults;
|
|
186
194
|
const dependencyLine = ` implementation project(':openiap-google')`;
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
// Groovy DSL: implementation "io.github.hyochan.openiap:openiap-google:x.y.z" or api "..."
|
|
190
|
-
/^\s*(?:implementation|api)\s+["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*$/gm,
|
|
191
|
-
// Kotlin DSL: implementation("io.github.hyochan.openiap:openiap-google:x.y.z") or api("...")
|
|
192
|
-
/^\s*(?:implementation|api)\s*\(\s*["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*\)\s*$/gm,
|
|
193
|
-
];
|
|
195
|
+
const flavor = props?.isHorizonEnabled ? 'horizon' : 'play';
|
|
196
|
+
const strategyLine = ` missingDimensionStrategy "platform", "${flavor}"`;
|
|
194
197
|
let contents = gradle.contents;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
198
|
+
// Remove Maven deps (avoid duplicate classes with local module)
|
|
199
|
+
const mavenPattern = /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*\)?\s*$/gm;
|
|
200
|
+
if (mavenPattern.test(contents)) {
|
|
201
|
+
contents = contents.replace(mavenPattern, '\n');
|
|
202
|
+
logOnce('🧹 Removed Maven openiap-google (using local module)');
|
|
201
203
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
// Add missingDimensionStrategy (required for flavored module)
|
|
205
|
+
// Remove any existing platform strategies first to avoid duplicates
|
|
206
|
+
const strategyPattern = /^\s*missingDimensionStrategy\s*\(?\s*["']platform["']\s*,\s*["'](play|horizon)["']\s*\)?\s*$/gm;
|
|
207
|
+
if (strategyPattern.test(contents)) {
|
|
208
|
+
contents = contents.replace(strategyPattern, '');
|
|
209
|
+
logOnce('🧹 Removed existing missingDimensionStrategy for platform');
|
|
205
210
|
}
|
|
206
|
-
if (!
|
|
211
|
+
if (!contents.includes(strategyLine)) {
|
|
212
|
+
const lines = contents.split('\n');
|
|
213
|
+
const idx = lines.findIndex((line) => line.match(/defaultConfig\s*\{/));
|
|
214
|
+
if (idx !== -1) {
|
|
215
|
+
lines.splice(idx + 1, 0, strategyLine);
|
|
216
|
+
contents = lines.join('\n');
|
|
217
|
+
logOnce(`🛠️ expo-iap: Added missingDimensionStrategy for ${flavor} flavor`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Add project dependency
|
|
221
|
+
if (!contents.includes(dependencyLine)) {
|
|
207
222
|
const anchor = /dependencies\s*\{/m;
|
|
208
|
-
if (anchor.test(
|
|
209
|
-
|
|
223
|
+
if (anchor.test(contents)) {
|
|
224
|
+
contents = contents.replace(anchor, (m) => `${m}\n${dependencyLine}`);
|
|
210
225
|
}
|
|
211
226
|
else {
|
|
212
|
-
|
|
227
|
+
contents += `\n\ndependencies {\n${dependencyLine}\n}\n`;
|
|
213
228
|
}
|
|
214
|
-
|
|
229
|
+
logOnce('🛠️ Added dependency on local :openiap-google project');
|
|
215
230
|
}
|
|
231
|
+
gradle.contents = contents;
|
|
216
232
|
return config;
|
|
217
233
|
});
|
|
218
|
-
// 3)
|
|
234
|
+
// 3) Set horizonEnabled in gradle.properties
|
|
219
235
|
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
220
236
|
'android',
|
|
221
237
|
async (config) => {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
for (const p of patterns) {
|
|
235
|
-
if (p.test(contents)) {
|
|
236
|
-
contents = contents.replace(p, '\n');
|
|
237
|
-
changed = true;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
if (changed) {
|
|
241
|
-
fs.writeFileSync(appBuildGradle, contents);
|
|
242
|
-
console.log('🧹 expo-iap: Cleaned Maven openiap-google for local :openiap-google');
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
catch (e) {
|
|
247
|
-
console.warn('expo-iap: cleanup step failed:', e);
|
|
238
|
+
const { platformProjectRoot } = config.modRequest;
|
|
239
|
+
const gradlePropertiesPath = path.join(platformProjectRoot, 'gradle.properties');
|
|
240
|
+
if (fs.existsSync(gradlePropertiesPath)) {
|
|
241
|
+
let contents = fs.readFileSync(gradlePropertiesPath, 'utf8');
|
|
242
|
+
const isHorizon = props?.isHorizonEnabled ?? false;
|
|
243
|
+
// Update horizonEnabled property
|
|
244
|
+
contents = contents.replace(/^horizonEnabled=.*$/gm, '');
|
|
245
|
+
if (!contents.endsWith('\n'))
|
|
246
|
+
contents += '\n';
|
|
247
|
+
contents += `horizonEnabled=${isHorizon}\n`;
|
|
248
|
+
fs.writeFileSync(gradlePropertiesPath, contents);
|
|
249
|
+
logOnce(`🛠️ expo-iap: Set horizonEnabled=${isHorizon} in gradle.properties`);
|
|
248
250
|
}
|
|
249
251
|
return config;
|
|
250
252
|
},
|
|
251
253
|
]);
|
|
252
|
-
// (removed) Avoid global root build.gradle mutations; included module should manage its plugins
|
|
253
254
|
return config;
|
|
254
255
|
};
|
|
255
256
|
exports.default = withLocalOpenIAP;
|
package/plugin/src/withIAP.ts
CHANGED
|
@@ -5,12 +5,14 @@ import {
|
|
|
5
5
|
withAndroidManifest,
|
|
6
6
|
withAppBuildGradle,
|
|
7
7
|
withDangerousMod,
|
|
8
|
-
withEntitlementsPlist,
|
|
9
|
-
withInfoPlist,
|
|
10
8
|
} from 'expo/config-plugins';
|
|
11
9
|
import * as fs from 'fs';
|
|
12
10
|
import * as path from 'path';
|
|
13
11
|
import withLocalOpenIAP from './withLocalOpenIAP';
|
|
12
|
+
import {
|
|
13
|
+
withIosAlternativeBilling,
|
|
14
|
+
type IOSAlternativeBillingConfig,
|
|
15
|
+
} from './withIosAlternativeBilling';
|
|
14
16
|
|
|
15
17
|
const pkg = require('../../package.json');
|
|
16
18
|
const openiapVersions = JSON.parse(
|
|
@@ -55,9 +57,13 @@ const addLineToGradle = (
|
|
|
55
57
|
const modifyAppBuildGradle = (
|
|
56
58
|
gradle: string,
|
|
57
59
|
language: 'groovy' | 'kotlin',
|
|
60
|
+
isHorizonEnabled?: boolean,
|
|
58
61
|
): string => {
|
|
59
62
|
let modified = gradle;
|
|
60
63
|
|
|
64
|
+
// Determine which flavor to use based on isHorizonEnabled
|
|
65
|
+
const flavor = isHorizonEnabled ? 'horizon' : 'play';
|
|
66
|
+
|
|
61
67
|
// Ensure OpenIAP dependency exists at desired version in app-level build.gradle(.kts)
|
|
62
68
|
const impl = (ga: string, v: string) =>
|
|
63
69
|
language === 'kotlin'
|
|
@@ -91,27 +97,66 @@ const modifyAppBuildGradle = (
|
|
|
91
97
|
);
|
|
92
98
|
}
|
|
93
99
|
|
|
100
|
+
// Add flavor dimension and default config for OpenIAP if horizon is enabled
|
|
101
|
+
if (isHorizonEnabled) {
|
|
102
|
+
// Add missingDimensionStrategy to select horizon flavor
|
|
103
|
+
const defaultConfigRegex = /defaultConfig\s*{/;
|
|
104
|
+
if (defaultConfigRegex.test(modified)) {
|
|
105
|
+
const strategyLine =
|
|
106
|
+
language === 'kotlin'
|
|
107
|
+
? ` missingDimensionStrategy("platform", "${flavor}")`
|
|
108
|
+
: ` missingDimensionStrategy "platform", "${flavor}"`;
|
|
109
|
+
|
|
110
|
+
// Remove any existing platform strategies first to avoid duplicates
|
|
111
|
+
const strategyPattern =
|
|
112
|
+
/^\s*missingDimensionStrategy\s*\(?\s*["']platform["']\s*,\s*["'](play|horizon)["']\s*\)?\s*$/gm;
|
|
113
|
+
if (strategyPattern.test(modified)) {
|
|
114
|
+
modified = modified.replace(strategyPattern, '');
|
|
115
|
+
logOnce('🧹 Removed existing missingDimensionStrategy for platform');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add the new strategy
|
|
119
|
+
if (!/missingDimensionStrategy.*platform/.test(modified)) {
|
|
120
|
+
modified = addLineToGradle(
|
|
121
|
+
modified,
|
|
122
|
+
defaultConfigRegex,
|
|
123
|
+
strategyLine,
|
|
124
|
+
1,
|
|
125
|
+
);
|
|
126
|
+
logOnce(
|
|
127
|
+
`🛠️ expo-iap: Added missingDimensionStrategy for ${flavor} flavor`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
94
133
|
return modified;
|
|
95
134
|
};
|
|
96
135
|
|
|
97
|
-
const withIapAndroid: ConfigPlugin<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
136
|
+
const withIapAndroid: ConfigPlugin<
|
|
137
|
+
{
|
|
138
|
+
addDeps?: boolean;
|
|
139
|
+
horizonAppId?: string;
|
|
140
|
+
isHorizonEnabled?: boolean;
|
|
141
|
+
} | void
|
|
142
|
+
> = (config, props) => {
|
|
101
143
|
const addDeps = props?.addDeps ?? true;
|
|
102
144
|
|
|
145
|
+
// Add dependencies if needed (only when not using local module)
|
|
103
146
|
if (addDeps) {
|
|
104
147
|
config = withAppBuildGradle(config, (config) => {
|
|
105
|
-
// language provided by config-plugins: 'groovy' | 'kotlin'
|
|
106
148
|
const language = (config.modResults as any).language || 'groovy';
|
|
107
149
|
config.modResults.contents = modifyAppBuildGradle(
|
|
108
150
|
config.modResults.contents,
|
|
109
151
|
language,
|
|
152
|
+
props?.isHorizonEnabled,
|
|
110
153
|
);
|
|
111
154
|
return config;
|
|
112
155
|
});
|
|
113
156
|
}
|
|
114
157
|
|
|
158
|
+
// Note: missingDimensionStrategy for local dev is handled in withLocalOpenIAP
|
|
159
|
+
|
|
115
160
|
config = withAndroidManifest(config, (config) => {
|
|
116
161
|
const manifest = config.modResults;
|
|
117
162
|
if (!manifest.manifest['uses-permission']) {
|
|
@@ -133,121 +178,45 @@ const withIapAndroid: ConfigPlugin<{addDeps?: boolean} | void> = (
|
|
|
133
178
|
);
|
|
134
179
|
}
|
|
135
180
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
links?: Record<string, string>;
|
|
147
|
-
/** Multiple external purchase URLs per country (iOS 17.5+, up to 5 per country) */
|
|
148
|
-
multiLinks?: Record<string, string[]>;
|
|
149
|
-
/** Custom link regions (iOS 18.1+) */
|
|
150
|
-
customLinkRegions?: string[];
|
|
151
|
-
/** Streaming link regions for music apps (iOS 18.2+) */
|
|
152
|
-
streamingLinkRegions?: string[];
|
|
153
|
-
/** Enable external purchase link entitlement */
|
|
154
|
-
enableExternalPurchaseLink?: boolean;
|
|
155
|
-
/** Enable external purchase link streaming entitlement (music apps only) */
|
|
156
|
-
enableExternalPurchaseLinkStreaming?: boolean;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Add external purchase entitlements and Info.plist configuration */
|
|
160
|
-
const withIosAlternativeBilling: ConfigPlugin<
|
|
161
|
-
IOSAlternativeBillingConfig | undefined
|
|
162
|
-
> = (config, options) => {
|
|
163
|
-
if (!options || !options.countries || options.countries.length === 0) {
|
|
164
|
-
return config;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Add entitlements
|
|
168
|
-
config = withEntitlementsPlist(config, (config) => {
|
|
169
|
-
// Always add basic external purchase entitlement when countries are specified
|
|
170
|
-
config.modResults['com.apple.developer.storekit.external-purchase'] = true;
|
|
171
|
-
logOnce(
|
|
172
|
-
'✅ Added com.apple.developer.storekit.external-purchase to entitlements',
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// Add external purchase link entitlement if enabled
|
|
176
|
-
if (options.enableExternalPurchaseLink) {
|
|
177
|
-
config.modResults['com.apple.developer.storekit.external-purchase-link'] =
|
|
178
|
-
true;
|
|
179
|
-
logOnce(
|
|
180
|
-
'✅ Added com.apple.developer.storekit.external-purchase-link to entitlements',
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Add streaming entitlement if enabled
|
|
185
|
-
if (options.enableExternalPurchaseLinkStreaming) {
|
|
186
|
-
config.modResults[
|
|
187
|
-
'com.apple.developer.storekit.external-purchase-link-streaming'
|
|
188
|
-
] = true;
|
|
189
|
-
logOnce(
|
|
190
|
-
'✅ Added com.apple.developer.storekit.external-purchase-link-streaming to entitlements',
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return config;
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// Add Info.plist configuration
|
|
198
|
-
config = withInfoPlist(config, (config) => {
|
|
199
|
-
const plist = config.modResults;
|
|
200
|
-
|
|
201
|
-
// 1. SKExternalPurchase (Required)
|
|
202
|
-
plist.SKExternalPurchase = options.countries;
|
|
203
|
-
logOnce(
|
|
204
|
-
`✅ Added SKExternalPurchase with countries: ${options.countries?.join(
|
|
205
|
-
', ',
|
|
206
|
-
)}`,
|
|
207
|
-
);
|
|
181
|
+
// Add Meta Horizon App ID if provided
|
|
182
|
+
if (props?.horizonAppId) {
|
|
183
|
+
if (
|
|
184
|
+
!manifest.manifest.application ||
|
|
185
|
+
manifest.manifest.application.length === 0
|
|
186
|
+
) {
|
|
187
|
+
manifest.manifest.application = [
|
|
188
|
+
{$: {'android:name': '.MainApplication'}},
|
|
189
|
+
];
|
|
190
|
+
}
|
|
208
191
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
`✅ Added SKExternalPurchaseLink for ${
|
|
214
|
-
Object.keys(options.links).length
|
|
215
|
-
} countries`,
|
|
216
|
-
);
|
|
217
|
-
}
|
|
192
|
+
const application = manifest.manifest.application![0];
|
|
193
|
+
if (!application['meta-data']) {
|
|
194
|
+
application['meta-data'] = [];
|
|
195
|
+
}
|
|
218
196
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
);
|
|
227
|
-
}
|
|
197
|
+
const metaData = application['meta-data'];
|
|
198
|
+
const horizonAppIdMeta = {
|
|
199
|
+
$: {
|
|
200
|
+
'android:name': 'com.oculus.vr.APP_ID',
|
|
201
|
+
'android:value': props.horizonAppId,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
228
204
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
plist.SKExternalPurchaseCustomLinkRegions = options.customLinkRegions;
|
|
232
|
-
logOnce(
|
|
233
|
-
`✅ Added SKExternalPurchaseCustomLinkRegions: ${options.customLinkRegions.join(
|
|
234
|
-
', ',
|
|
235
|
-
)}`,
|
|
205
|
+
const existingIndex = metaData.findIndex(
|
|
206
|
+
(m) => m.$['android:name'] === 'com.oculus.vr.APP_ID',
|
|
236
207
|
);
|
|
237
|
-
}
|
|
238
208
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
);
|
|
209
|
+
if (existingIndex !== -1) {
|
|
210
|
+
metaData[existingIndex] = horizonAppIdMeta;
|
|
211
|
+
logOnce(
|
|
212
|
+
`✅ Updated com.oculus.vr.APP_ID to ${props.horizonAppId} in AndroidManifest.xml`,
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
metaData.push(horizonAppIdMeta);
|
|
216
|
+
logOnce(
|
|
217
|
+
`✅ Added com.oculus.vr.APP_ID: ${props.horizonAppId} to AndroidManifest.xml`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
251
220
|
}
|
|
252
221
|
|
|
253
222
|
return config;
|
|
@@ -310,12 +279,47 @@ export interface ExpoIapPluginOptions {
|
|
|
310
279
|
/** Enable local development mode */
|
|
311
280
|
enableLocalDev?: boolean;
|
|
312
281
|
/**
|
|
313
|
-
*
|
|
314
|
-
|
|
315
|
-
|
|
282
|
+
* Optional modules configuration
|
|
283
|
+
*/
|
|
284
|
+
modules?: {
|
|
285
|
+
/**
|
|
286
|
+
* Onside module for iOS alternative billing (Korea market)
|
|
287
|
+
* @platform ios
|
|
288
|
+
*/
|
|
289
|
+
onside?: boolean;
|
|
290
|
+
/**
|
|
291
|
+
* Horizon module for Meta Quest/VR devices
|
|
292
|
+
* @platform android
|
|
293
|
+
*/
|
|
294
|
+
horizon?: boolean;
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* iOS-specific configuration
|
|
316
298
|
* @platform ios
|
|
317
299
|
*/
|
|
300
|
+
ios?: {
|
|
301
|
+
/**
|
|
302
|
+
* iOS Alternative Billing configuration.
|
|
303
|
+
* Configure external purchase countries, links, and entitlements.
|
|
304
|
+
* Requires approval from Apple.
|
|
305
|
+
*/
|
|
306
|
+
alternativeBilling?: IOSAlternativeBillingConfig;
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Android-specific configuration
|
|
310
|
+
* @platform android
|
|
311
|
+
*/
|
|
312
|
+
android?: {
|
|
313
|
+
/**
|
|
314
|
+
* Meta Horizon App ID for Quest/VR devices.
|
|
315
|
+
* Required when modules.horizon is true.
|
|
316
|
+
*/
|
|
317
|
+
horizonAppId?: string;
|
|
318
|
+
};
|
|
319
|
+
/** @deprecated Use ios.alternativeBilling instead */
|
|
318
320
|
iosAlternativeBilling?: IOSAlternativeBillingConfig;
|
|
321
|
+
/** @deprecated Use android.horizonAppId instead */
|
|
322
|
+
horizonAppId?: string;
|
|
319
323
|
}
|
|
320
324
|
|
|
321
325
|
const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
|
|
@@ -323,10 +327,26 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
|
|
|
323
327
|
options,
|
|
324
328
|
) => {
|
|
325
329
|
try {
|
|
330
|
+
// Read Horizon configuration from modules
|
|
331
|
+
const isHorizonEnabled = options?.modules?.horizon ?? false;
|
|
332
|
+
|
|
333
|
+
const horizonAppId =
|
|
334
|
+
options?.android?.horizonAppId ?? options?.horizonAppId;
|
|
335
|
+
const iosAlternativeBilling =
|
|
336
|
+
options?.ios?.alternativeBilling ?? options?.iosAlternativeBilling;
|
|
337
|
+
|
|
338
|
+
logOnce(
|
|
339
|
+
`🔍 [expo-iap] Config values: horizonAppId=${horizonAppId}, isHorizonEnabled=${isHorizonEnabled}`,
|
|
340
|
+
);
|
|
341
|
+
|
|
326
342
|
// Respect explicit flag; fall back to presence of localPath only when flag is unset
|
|
327
343
|
const isLocalDev = options?.enableLocalDev ?? !!options?.localPath;
|
|
328
344
|
// Apply Android modifications (skip adding deps when linking local module)
|
|
329
|
-
let result = withIapAndroid(config, {
|
|
345
|
+
let result = withIapAndroid(config, {
|
|
346
|
+
addDeps: !isLocalDev,
|
|
347
|
+
horizonAppId,
|
|
348
|
+
isHorizonEnabled,
|
|
349
|
+
});
|
|
330
350
|
|
|
331
351
|
// iOS: choose one path to avoid overlap
|
|
332
352
|
if (isLocalDev) {
|
|
@@ -354,12 +374,14 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
|
|
|
354
374
|
logOnce(`🔧 [expo-iap] Enabling local OpenIAP: ${preview}`);
|
|
355
375
|
result = withLocalOpenIAP(result, {
|
|
356
376
|
localPath: resolved,
|
|
357
|
-
iosAlternativeBilling
|
|
377
|
+
iosAlternativeBilling,
|
|
378
|
+
horizonAppId,
|
|
379
|
+
isHorizonEnabled, // Resolved from modules.horizon (line 467)
|
|
358
380
|
});
|
|
359
381
|
}
|
|
360
382
|
} else {
|
|
361
383
|
// Ensure iOS Podfile is set up to resolve public CocoaPods specs
|
|
362
|
-
result = withIapIOS(result,
|
|
384
|
+
result = withIapIOS(result, iosAlternativeBilling);
|
|
363
385
|
logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
|
|
364
386
|
}
|
|
365
387
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigPlugin,
|
|
3
|
+
withEntitlementsPlist,
|
|
4
|
+
withInfoPlist,
|
|
5
|
+
} from 'expo/config-plugins';
|
|
6
|
+
|
|
7
|
+
// Log a message only once per Node process
|
|
8
|
+
const logOnce = (() => {
|
|
9
|
+
const printed = new Set<string>();
|
|
10
|
+
return (msg: string) => {
|
|
11
|
+
if (!printed.has(msg)) {
|
|
12
|
+
console.log(msg);
|
|
13
|
+
printed.add(msg);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
})();
|
|
17
|
+
|
|
18
|
+
export interface IOSAlternativeBillingConfig {
|
|
19
|
+
/** Country codes where external purchases are supported (ISO 3166-1 alpha-2) */
|
|
20
|
+
countries?: string[];
|
|
21
|
+
/** External purchase URLs per country (iOS 15.4+) */
|
|
22
|
+
links?: Record<string, string>;
|
|
23
|
+
/** Multiple external purchase URLs per country (iOS 17.5+, up to 5 per country) */
|
|
24
|
+
multiLinks?: Record<string, string[]>;
|
|
25
|
+
/** Custom link regions (iOS 18.1+) */
|
|
26
|
+
customLinkRegions?: string[];
|
|
27
|
+
/** Streaming link regions for music apps (iOS 18.2+) */
|
|
28
|
+
streamingLinkRegions?: string[];
|
|
29
|
+
/** Enable external purchase link entitlement */
|
|
30
|
+
enableExternalPurchaseLink?: boolean;
|
|
31
|
+
/** Enable external purchase link streaming entitlement (music apps only) */
|
|
32
|
+
enableExternalPurchaseLinkStreaming?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Add external purchase entitlements and Info.plist configuration */
|
|
36
|
+
export const withIosAlternativeBilling: ConfigPlugin<
|
|
37
|
+
IOSAlternativeBillingConfig | undefined
|
|
38
|
+
> = (config, options) => {
|
|
39
|
+
if (!options || !options.countries || options.countries.length === 0) {
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Add entitlements
|
|
44
|
+
config = withEntitlementsPlist(config, (config) => {
|
|
45
|
+
// Always add basic external purchase entitlement when countries are specified
|
|
46
|
+
config.modResults['com.apple.developer.storekit.external-purchase'] = true;
|
|
47
|
+
logOnce(
|
|
48
|
+
'✅ Added com.apple.developer.storekit.external-purchase to entitlements',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Add external purchase link entitlement if enabled
|
|
52
|
+
if (options.enableExternalPurchaseLink) {
|
|
53
|
+
config.modResults['com.apple.developer.storekit.external-purchase-link'] =
|
|
54
|
+
true;
|
|
55
|
+
logOnce(
|
|
56
|
+
'✅ Added com.apple.developer.storekit.external-purchase-link to entitlements',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add streaming entitlement if enabled
|
|
61
|
+
if (options.enableExternalPurchaseLinkStreaming) {
|
|
62
|
+
config.modResults[
|
|
63
|
+
'com.apple.developer.storekit.external-purchase-link-streaming'
|
|
64
|
+
] = true;
|
|
65
|
+
logOnce(
|
|
66
|
+
'✅ Added com.apple.developer.storekit.external-purchase-link-streaming to entitlements',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return config;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Add Info.plist configuration
|
|
74
|
+
config = withInfoPlist(config, (config) => {
|
|
75
|
+
const plist = config.modResults;
|
|
76
|
+
|
|
77
|
+
// 1. SKExternalPurchase (Required)
|
|
78
|
+
plist.SKExternalPurchase = options.countries;
|
|
79
|
+
logOnce(
|
|
80
|
+
`✅ Added SKExternalPurchase with countries: ${options.countries?.join(
|
|
81
|
+
', ',
|
|
82
|
+
)}`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// 2. SKExternalPurchaseLink (Optional - iOS 15.4+)
|
|
86
|
+
if (options.links && Object.keys(options.links).length > 0) {
|
|
87
|
+
plist.SKExternalPurchaseLink = options.links;
|
|
88
|
+
logOnce(
|
|
89
|
+
`✅ Added SKExternalPurchaseLink for ${
|
|
90
|
+
Object.keys(options.links).length
|
|
91
|
+
} countries`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. SKExternalPurchaseMultiLink (iOS 17.5+)
|
|
96
|
+
if (options.multiLinks && Object.keys(options.multiLinks).length > 0) {
|
|
97
|
+
plist.SKExternalPurchaseMultiLink = options.multiLinks;
|
|
98
|
+
logOnce(
|
|
99
|
+
`✅ Added SKExternalPurchaseMultiLink for ${
|
|
100
|
+
Object.keys(options.multiLinks).length
|
|
101
|
+
} countries`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. SKExternalPurchaseCustomLinkRegions (iOS 18.1+)
|
|
106
|
+
if (options.customLinkRegions && options.customLinkRegions.length > 0) {
|
|
107
|
+
plist.SKExternalPurchaseCustomLinkRegions = options.customLinkRegions;
|
|
108
|
+
logOnce(
|
|
109
|
+
`✅ Added SKExternalPurchaseCustomLinkRegions: ${options.customLinkRegions.join(
|
|
110
|
+
', ',
|
|
111
|
+
)}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 5. SKExternalPurchaseLinkStreamingRegions (iOS 18.2+)
|
|
116
|
+
if (
|
|
117
|
+
options.streamingLinkRegions &&
|
|
118
|
+
options.streamingLinkRegions.length > 0
|
|
119
|
+
) {
|
|
120
|
+
plist.SKExternalPurchaseLinkStreamingRegions =
|
|
121
|
+
options.streamingLinkRegions;
|
|
122
|
+
logOnce(
|
|
123
|
+
`✅ Added SKExternalPurchaseLinkStreamingRegions: ${options.streamingLinkRegions.join(
|
|
124
|
+
', ',
|
|
125
|
+
)}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return config;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return config;
|
|
133
|
+
};
|