expo-iap 3.1.0 → 3.1.1-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/build/ExpoIapModule.d.ts +1 -0
  2. package/build/ExpoIapModule.d.ts.map +1 -1
  3. package/build/ExpoIapModule.js +29 -4
  4. package/build/ExpoIapModule.js.map +1 -1
  5. package/build/utils/constants.d.ts +2 -0
  6. package/build/utils/constants.d.ts.map +1 -1
  7. package/build/utils/constants.js +9 -2
  8. package/build/utils/constants.js.map +1 -1
  9. package/coverage/clover.xml +497 -0
  10. package/coverage/coverage-final.json +6 -0
  11. package/coverage/lcov-report/base.css +224 -0
  12. package/coverage/lcov-report/block-navigation.js +87 -0
  13. package/coverage/lcov-report/favicon.png +0 -0
  14. package/coverage/lcov-report/index.html +161 -0
  15. package/coverage/lcov-report/prettify.css +1 -0
  16. package/coverage/lcov-report/prettify.js +2 -0
  17. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  18. package/coverage/lcov-report/sorter.js +196 -0
  19. package/coverage/lcov-report/src/ExpoIap.types.ts.html +1243 -0
  20. package/coverage/lcov-report/src/PurchaseError.ts.html +787 -0
  21. package/coverage/lcov-report/src/helpers/index.html +116 -0
  22. package/coverage/lcov-report/src/helpers/subscription.ts.html +496 -0
  23. package/coverage/lcov-report/src/index.html +116 -0
  24. package/coverage/lcov-report/src/index.ts.html +1993 -0
  25. package/coverage/lcov-report/src/modules/android.ts.html +550 -0
  26. package/coverage/lcov-report/src/modules/index.html +131 -0
  27. package/coverage/lcov-report/src/modules/ios.ts.html +1222 -0
  28. package/coverage/lcov-report/src/purchase-error.ts.html +880 -0
  29. package/coverage/lcov-report/src/types/ExpoIapAndroid.types.ts.html +493 -0
  30. package/coverage/lcov-report/src/types/index.html +116 -0
  31. package/coverage/lcov-report/src/useIap.ts.html +1483 -0
  32. package/coverage/lcov-report/src/utils/errorMapping.ts.html +1069 -0
  33. package/coverage/lcov-report/src/utils/index.html +116 -0
  34. package/coverage/lcov-report/src/utils/purchase.ts.html +241 -0
  35. package/coverage/lcov.info +929 -0
  36. package/expo-module.config.json +10 -3
  37. package/ios/onside/OnsideIapModule.swift +489 -0
  38. package/package.json +4 -3
  39. package/plugin/build/withIAP.d.ts +22 -9
  40. package/plugin/build/withIAP.js +157 -9
  41. package/plugin/jest.config.js +13 -3
  42. package/plugin/src/expoConfig.augmentation.d.ts +38 -0
  43. package/plugin/src/withIAP.ts +258 -18
  44. package/plugin/tsconfig.json +2 -1
  45. package/plugin/tsconfig.tsbuildinfo +1 -1
  46. package/src/ExpoIapModule.ts +45 -4
  47. package/src/utils/constants.ts +11 -2
@@ -36,6 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.modifyAppBuildGradle = void 0;
40
+ exports.computeAutolinkModules = computeAutolinkModules;
41
+ exports.resolveModuleSelection = resolveModuleSelection;
39
42
  const config_plugins_1 = require("expo/config-plugins");
40
43
  const fs = __importStar(require("fs"));
41
44
  const path = __importStar(require("path"));
@@ -43,6 +46,7 @@ const withLocalOpenIAP_1 = __importDefault(require("./withLocalOpenIAP"));
43
46
  const pkg = require('../../package.json');
44
47
  const openiapVersions = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../openiap-versions.json'), 'utf8'));
45
48
  const OPENIAP_ANDROID_VERSION = openiapVersions.google;
49
+ const AUTOLINKING_CONFIG_PATH = path.resolve(__dirname, '../../expo-module.config.json');
46
50
  // Log a message only once per Node process
47
51
  const logOnce = (() => {
48
52
  const printed = new Set();
@@ -88,13 +92,14 @@ const modifyAppBuildGradle = (gradle, language) => {
88
92
  }
89
93
  return modified;
90
94
  };
95
+ exports.modifyAppBuildGradle = modifyAppBuildGradle;
91
96
  const withIapAndroid = (config, props) => {
92
97
  const addDeps = props?.addDeps ?? true;
93
98
  if (addDeps) {
94
99
  config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
95
100
  // language provided by config-plugins: 'groovy' | 'kotlin'
96
101
  const language = config.modResults.language || 'groovy';
97
- config.modResults.contents = modifyAppBuildGradle(config.modResults.contents, language);
102
+ config.modResults.contents = (0, exports.modifyAppBuildGradle)(config.modResults.contents, language);
98
103
  return config;
99
104
  });
100
105
  }
@@ -117,8 +122,88 @@ const withIapAndroid = (config, props) => {
117
122
  });
118
123
  return config;
119
124
  };
120
- /** Ensure Podfile uses CocoaPods CDN and no stale local OpenIAP entry remains. */
121
- const withIapIOS = (config) => {
125
+ const ensureOnsidePod = (content) => {
126
+ const podLine = " pod 'OnsideKit', :git => 'https://github.com/onside-io/OnsideKit-iOS.git'";
127
+ const podRegex = /^\s*pod\s+'OnsideKit'\b.*$/m;
128
+ if (podRegex.test(content)) {
129
+ return content;
130
+ }
131
+ const targetMatch = content.match(/target\s+'[^']+'\s+do\s*\n/);
132
+ if (!targetMatch) {
133
+ config_plugins_1.WarningAggregator.addWarningIOS('expo-iap', 'Could not find a target block in Podfile when adding OnsideKit; skipping installation.');
134
+ return content;
135
+ }
136
+ const insertIndex = targetMatch.index + targetMatch[0].length;
137
+ const before = content.slice(0, insertIndex);
138
+ const after = content.slice(insertIndex);
139
+ logOnce('📦 expo-iap: Added OnsideKit pod to Podfile');
140
+ return `${before}${podLine}\n${after}`;
141
+ };
142
+ function computeAutolinkModules(existing, desired) {
143
+ let modules = [...existing];
144
+ const added = [];
145
+ const removed = [];
146
+ for (const entry of desired) {
147
+ const hasModule = modules.includes(entry.name);
148
+ if (entry.enable && !hasModule) {
149
+ modules = [...modules, entry.name];
150
+ added.push(entry.name);
151
+ }
152
+ else if (!entry.enable && hasModule) {
153
+ modules = modules.filter((module) => module !== entry.name);
154
+ removed.push(entry.name);
155
+ }
156
+ }
157
+ return { modules, added, removed };
158
+ }
159
+ const syncAutolinking = (state) => {
160
+ if (!fs.existsSync(AUTOLINKING_CONFIG_PATH)) {
161
+ return;
162
+ }
163
+ try {
164
+ const raw = fs.readFileSync(AUTOLINKING_CONFIG_PATH, 'utf8');
165
+ const config = JSON.parse(raw);
166
+ const iosConfig = config.ios ?? (config.ios = {});
167
+ const existing = Array.isArray(iosConfig.modules)
168
+ ? iosConfig.modules.filter((module) => module !== 'OneSideModule')
169
+ : [];
170
+ const desiredEntries = [
171
+ {
172
+ name: 'ExpoIapModule',
173
+ enable: state.expoIap,
174
+ addLog: '🔗 expo-iap: Enabled ExpoIapModule autolinking',
175
+ removeLog: '🧹 expo-iap: Disabled ExpoIapModule autolinking',
176
+ },
177
+ {
178
+ name: 'OnsideIapModule',
179
+ enable: state.onside,
180
+ addLog: '🔗 expo-iap: Enabled OnsideIapModule autolinking',
181
+ removeLog: '🧹 expo-iap: Disabled OnsideIapModule autolinking',
182
+ },
183
+ ];
184
+ const { modules: nextModules, added, removed, } = computeAutolinkModules(existing, desiredEntries.map(({ name, enable }) => ({ name, enable })));
185
+ for (const name of added) {
186
+ const entry = desiredEntries.find((candidate) => candidate.name === name);
187
+ if (entry) {
188
+ logOnce(entry.addLog);
189
+ }
190
+ }
191
+ for (const name of removed) {
192
+ const entry = desiredEntries.find((candidate) => candidate.name === name);
193
+ if (entry) {
194
+ logOnce(entry.removeLog);
195
+ }
196
+ }
197
+ if (added.length > 0 || removed.length > 0) {
198
+ iosConfig.modules = nextModules;
199
+ fs.writeFileSync(AUTOLINKING_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
200
+ }
201
+ }
202
+ catch (error) {
203
+ config_plugins_1.WarningAggregator.addWarningIOS('expo-iap', `Failed to sync Expo IAP autolinking modules: ${String(error)}`);
204
+ }
205
+ };
206
+ const withIapIOS = (config, props) => {
122
207
  return (0, config_plugins_1.withDangerousMod)(config, [
123
208
  'ios',
124
209
  async (config) => {
@@ -140,17 +225,77 @@ const withIapIOS = (config) => {
140
225
  content = content.replace(localPodRegex, '').replace(/\n{3,}/g, '\n\n');
141
226
  logOnce('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
142
227
  }
228
+ // 3) Optionally install OnsideKit when enabled in config
229
+ if (props?.enableOnside) {
230
+ content = ensureOnsidePod(content);
231
+ }
143
232
  fs.writeFileSync(podfilePath, content);
144
233
  return config;
145
234
  },
146
235
  ]);
147
236
  };
148
- const withIap = (config, options) => {
237
+ const MODULE_RULES = {
238
+ expoIap: {
239
+ when: {
240
+ 'expo-iap': true,
241
+ onside: false,
242
+ },
243
+ default: ({ options }) => options?.modules?.expoIap ?? true,
244
+ },
245
+ onside: {
246
+ when: {
247
+ 'expo-iap': false,
248
+ onside: true,
249
+ },
250
+ default: ({ config, options }) => options?.modules?.onside ?? config.ios?.onside?.enabled ?? true,
251
+ },
252
+ };
253
+ function resolveModuleSelection(config, options) {
254
+ const normalizedOptions = (options ?? undefined);
255
+ const selection = normalizedOptions?.module ?? 'auto';
256
+ const includeExpoIap = pickModuleState('expoIap', selection, config, normalizedOptions);
257
+ const includeOnside = pickModuleState('onside', selection, config, normalizedOptions);
258
+ return { selection, includeExpoIap, includeOnside };
259
+ }
260
+ function pickModuleState(key, selection, config, options) {
261
+ const rules = MODULE_RULES[key];
262
+ const explicit = rules.when[selection];
263
+ if (explicit !== undefined) {
264
+ return explicit;
265
+ }
266
+ const override = options?.modules?.[key];
267
+ if (override !== undefined) {
268
+ return override;
269
+ }
270
+ return rules.default({ config, options });
271
+ }
272
+ const withIAP = (config, options) => {
149
273
  try {
274
+ const { includeExpoIap, includeOnside } = resolveModuleSelection(config, options);
275
+ const autolinkState = {
276
+ expoIap: includeExpoIap,
277
+ onside: includeOnside,
278
+ };
279
+ if (includeOnside) {
280
+ config.ios = {
281
+ ...config.ios,
282
+ onside: {
283
+ ...(config.ios?.onside ?? {}),
284
+ enabled: true,
285
+ },
286
+ };
287
+ }
288
+ else if (config.ios?.onside?.enabled) {
289
+ config.ios.onside.enabled = false;
290
+ }
150
291
  // Respect explicit flag; fall back to presence of localPath only when flag is unset
151
292
  const isLocalDev = options?.enableLocalDev ?? !!options?.localPath;
152
- // Apply Android modifications (skip adding deps when linking local module)
153
- let result = withIapAndroid(config, { addDeps: !isLocalDev });
293
+ const shouldConfigureAndroid = includeExpoIap;
294
+ const shouldAddAndroidDeps = includeExpoIap && !isLocalDev;
295
+ // Apply Android modifications (skip when Expo IAP disabled)
296
+ let result = shouldConfigureAndroid
297
+ ? withIapAndroid(config, { addDeps: shouldAddAndroidDeps })
298
+ : config;
154
299
  // iOS: choose one path to avoid overlap
155
300
  if (isLocalDev) {
156
301
  if (!options?.localPath) {
@@ -173,9 +318,12 @@ const withIap = (config, options) => {
173
318
  }
174
319
  else {
175
320
  // Ensure iOS Podfile is set up to resolve public CocoaPods specs
176
- result = withIapIOS(result);
177
- logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
321
+ result = withIapIOS(result, { enableOnside: includeOnside });
322
+ if (includeExpoIap) {
323
+ logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
324
+ }
178
325
  }
326
+ syncAutolinking(autolinkState);
179
327
  return result;
180
328
  }
181
329
  catch (error) {
@@ -184,4 +332,4 @@ const withIap = (config, options) => {
184
332
  return config;
185
333
  }
186
334
  };
187
- exports.default = (0, config_plugins_1.createRunOncePlugin)(withIap, pkg.name, pkg.version);
335
+ exports.default = (0, config_plugins_1.createRunOncePlugin)(withIAP, pkg.name, pkg.version);
@@ -1,5 +1,15 @@
1
- // In documentation there is `preset: expo-module-scripts`, but it runs tests for every platform (ios, android, web, node)
2
- // We need only node tests right now
3
1
  module.exports = {
4
- preset: 'jest-expo/node',
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ transform: {
5
+ '^.+\\.(ts|tsx)$': [
6
+ 'ts-jest',
7
+ {
8
+ tsconfig: '<rootDir>/tsconfig.json',
9
+ },
10
+ ],
11
+ },
12
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
13
+ roots: ['<rootDir>/__tests__'],
14
+ testMatch: ['**/?(*.)+(spec|test).ts?(x)'],
5
15
  };
@@ -0,0 +1,38 @@
1
+ import type {IOS} from '@expo/config-types';
2
+
3
+ export type ExpoIapModuleOverrides = {
4
+ expoIap?: boolean;
5
+ onside?: boolean;
6
+ };
7
+
8
+ type BaseExpoIapOptions = {
9
+ enableLocalDev?: boolean;
10
+ localPath?:
11
+ | string
12
+ | {
13
+ ios?: string;
14
+ android?: string;
15
+ };
16
+ };
17
+
18
+ type AutoModuleOptions = BaseExpoIapOptions & {
19
+ module?: 'auto';
20
+ modules?: ExpoIapModuleOverrides;
21
+ };
22
+
23
+ type ExplicitModuleOptions = BaseExpoIapOptions & {
24
+ module: 'expo-iap' | 'onside';
25
+ modules?: never;
26
+ };
27
+
28
+ export type ExpoIapPluginCommonOptions =
29
+ | AutoModuleOptions
30
+ | ExplicitModuleOptions;
31
+
32
+ declare module '@expo/config-types' {
33
+ interface IOS {
34
+ onside?: {
35
+ enabled?: boolean;
36
+ };
37
+ }
38
+ }
@@ -6,9 +6,11 @@ import {
6
6
  withAppBuildGradle,
7
7
  withDangerousMod,
8
8
  } from 'expo/config-plugins';
9
+ import type {ExpoConfig} from '@expo/config-types';
9
10
  import * as fs from 'fs';
10
11
  import * as path from 'path';
11
12
  import withLocalOpenIAP from './withLocalOpenIAP';
13
+ import type {ExpoIapPluginCommonOptions} from './expoConfig.augmentation';
12
14
 
13
15
  const pkg = require('../../package.json');
14
16
  const openiapVersions = JSON.parse(
@@ -18,6 +20,10 @@ const openiapVersions = JSON.parse(
18
20
  ),
19
21
  );
20
22
  const OPENIAP_ANDROID_VERSION = openiapVersions.google;
23
+ const AUTOLINKING_CONFIG_PATH = path.resolve(
24
+ __dirname,
25
+ '../../expo-module.config.json',
26
+ );
21
27
 
22
28
  // Log a message only once per Node process
23
29
  const logOnce = (() => {
@@ -50,7 +56,7 @@ const addLineToGradle = (
50
56
  return lines.join('\n');
51
57
  };
52
58
 
53
- const modifyAppBuildGradle = (
59
+ export const modifyAppBuildGradle = (
54
60
  gradle: string,
55
61
  language: 'groovy' | 'kotlin',
56
62
  ): string => {
@@ -138,7 +144,136 @@ const withIapAndroid: ConfigPlugin<{addDeps?: boolean} | void> = (
138
144
  };
139
145
 
140
146
  /** Ensure Podfile uses CocoaPods CDN and no stale local OpenIAP entry remains. */
141
- const withIapIOS: ConfigPlugin = (config) => {
147
+ type WithIapIosOptions = {
148
+ enableOnside?: boolean;
149
+ };
150
+
151
+ const ensureOnsidePod = (content: string): string => {
152
+ const podLine =
153
+ " pod 'OnsideKit', :git => 'https://github.com/onside-io/OnsideKit-iOS.git'";
154
+ const podRegex = /^\s*pod\s+'OnsideKit'\b.*$/m;
155
+
156
+ if (podRegex.test(content)) {
157
+ return content;
158
+ }
159
+
160
+ const targetMatch = content.match(/target\s+'[^']+'\s+do\s*\n/);
161
+ if (!targetMatch) {
162
+ WarningAggregator.addWarningIOS(
163
+ 'expo-iap',
164
+ 'Could not find a target block in Podfile when adding OnsideKit; skipping installation.',
165
+ );
166
+ return content;
167
+ }
168
+
169
+ const insertIndex = targetMatch.index! + targetMatch[0].length;
170
+ const before = content.slice(0, insertIndex);
171
+ const after = content.slice(insertIndex);
172
+
173
+ logOnce('📦 expo-iap: Added OnsideKit pod to Podfile');
174
+
175
+ return `${before}${podLine}\n${after}`;
176
+ };
177
+
178
+ export type AutolinkState = {expoIap: boolean; onside: boolean};
179
+
180
+ type AutolinkEntry = {name: string; enable: boolean};
181
+
182
+ export function computeAutolinkModules(
183
+ existing: string[],
184
+ desired: AutolinkEntry[],
185
+ ): {modules: string[]; added: string[]; removed: string[]} {
186
+ let modules = [...existing];
187
+ const added: string[] = [];
188
+ const removed: string[] = [];
189
+
190
+ for (const entry of desired) {
191
+ const hasModule = modules.includes(entry.name);
192
+ if (entry.enable && !hasModule) {
193
+ modules = [...modules, entry.name];
194
+ added.push(entry.name);
195
+ } else if (!entry.enable && hasModule) {
196
+ modules = modules.filter((module) => module !== entry.name);
197
+ removed.push(entry.name);
198
+ }
199
+ }
200
+
201
+ return {modules, added, removed};
202
+ }
203
+
204
+ const syncAutolinking = (state: AutolinkState) => {
205
+ if (!fs.existsSync(AUTOLINKING_CONFIG_PATH)) {
206
+ return;
207
+ }
208
+
209
+ try {
210
+ const raw = fs.readFileSync(AUTOLINKING_CONFIG_PATH, 'utf8');
211
+ const config = JSON.parse(raw);
212
+ const iosConfig = config.ios ?? (config.ios = {});
213
+ const existing: string[] = Array.isArray(iosConfig.modules)
214
+ ? iosConfig.modules.filter((module: string) => module !== 'OneSideModule')
215
+ : [];
216
+
217
+ const desiredEntries: {
218
+ name: string;
219
+ enable: boolean;
220
+ addLog: string;
221
+ removeLog: string;
222
+ }[] = [
223
+ {
224
+ name: 'ExpoIapModule',
225
+ enable: state.expoIap,
226
+ addLog: '🔗 expo-iap: Enabled ExpoIapModule autolinking',
227
+ removeLog: '🧹 expo-iap: Disabled ExpoIapModule autolinking',
228
+ },
229
+ {
230
+ name: 'OnsideIapModule',
231
+ enable: state.onside,
232
+ addLog: '🔗 expo-iap: Enabled OnsideIapModule autolinking',
233
+ removeLog: '🧹 expo-iap: Disabled OnsideIapModule autolinking',
234
+ },
235
+ ];
236
+
237
+ const {
238
+ modules: nextModules,
239
+ added,
240
+ removed,
241
+ } = computeAutolinkModules(
242
+ existing,
243
+ desiredEntries.map(({name, enable}) => ({name, enable})),
244
+ );
245
+
246
+ for (const name of added) {
247
+ const entry = desiredEntries.find((candidate) => candidate.name === name);
248
+ if (entry) {
249
+ logOnce(entry.addLog);
250
+ }
251
+ }
252
+
253
+ for (const name of removed) {
254
+ const entry = desiredEntries.find((candidate) => candidate.name === name);
255
+ if (entry) {
256
+ logOnce(entry.removeLog);
257
+ }
258
+ }
259
+
260
+ if (added.length > 0 || removed.length > 0) {
261
+ iosConfig.modules = nextModules;
262
+ fs.writeFileSync(
263
+ AUTOLINKING_CONFIG_PATH,
264
+ `${JSON.stringify(config, null, 2)}\n`,
265
+ 'utf8',
266
+ );
267
+ }
268
+ } catch (error) {
269
+ WarningAggregator.addWarningIOS(
270
+ 'expo-iap',
271
+ `Failed to sync Expo IAP autolinking modules: ${String(error)}`,
272
+ );
273
+ }
274
+ };
275
+
276
+ const withIapIOS: ConfigPlugin<WithIapIosOptions | void> = (config, props) => {
142
277
  return withDangerousMod(config, [
143
278
  'ios',
144
279
  async (config) => {
@@ -166,33 +301,134 @@ const withIapIOS: ConfigPlugin = (config) => {
166
301
  logOnce('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
167
302
  }
168
303
 
304
+ // 3) Optionally install OnsideKit when enabled in config
305
+ if (props?.enableOnside) {
306
+ content = ensureOnsidePod(content);
307
+ }
308
+
169
309
  fs.writeFileSync(podfilePath, content);
170
310
  return config;
171
311
  },
172
312
  ]);
173
313
  };
174
314
 
175
- export interface ExpoIapPluginOptions {
176
- /** Local development path for OpenIAP library */
177
- localPath?:
178
- | string
179
- | {
180
- ios?: string;
181
- android?: string;
182
- };
183
- /** Enable local development mode */
184
- enableLocalDev?: boolean;
315
+ export interface ModuleSelectionResult {
316
+ selection: 'auto' | 'expo-iap' | 'onside';
317
+ includeExpoIap: boolean;
318
+ includeOnside: boolean;
319
+ }
320
+
321
+ type ModuleKey = 'expoIap' | 'onside';
322
+
323
+ type ModuleRules = Record<
324
+ ModuleKey,
325
+ {
326
+ when: Partial<Record<ModuleSelectionResult['selection'], boolean>>;
327
+ default: (args: {
328
+ config: ExpoConfig;
329
+ options?: ExpoIapPluginCommonOptions;
330
+ }) => boolean;
331
+ }
332
+ >;
333
+
334
+ const MODULE_RULES: ModuleRules = {
335
+ expoIap: {
336
+ when: {
337
+ 'expo-iap': true,
338
+ onside: false,
339
+ },
340
+ default: ({options}) => options?.modules?.expoIap ?? true,
341
+ },
342
+ onside: {
343
+ when: {
344
+ 'expo-iap': false,
345
+ onside: true,
346
+ },
347
+ default: ({config, options}) =>
348
+ options?.modules?.onside ?? config.ios?.onside?.enabled ?? true,
349
+ },
350
+ };
351
+
352
+ export function resolveModuleSelection(
353
+ config: ExpoConfig,
354
+ options?: ExpoIapPluginCommonOptions | void,
355
+ ): ModuleSelectionResult {
356
+ const normalizedOptions = (options ?? undefined) as
357
+ | ExpoIapPluginCommonOptions
358
+ | undefined;
359
+
360
+ const selection = normalizedOptions?.module ?? 'auto';
361
+
362
+ const includeExpoIap = pickModuleState(
363
+ 'expoIap',
364
+ selection,
365
+ config,
366
+ normalizedOptions,
367
+ );
368
+ const includeOnside = pickModuleState(
369
+ 'onside',
370
+ selection,
371
+ config,
372
+ normalizedOptions,
373
+ );
374
+
375
+ return {selection, includeExpoIap, includeOnside};
376
+ }
377
+
378
+ function pickModuleState(
379
+ key: ModuleKey,
380
+ selection: ModuleSelectionResult['selection'],
381
+ config: ExpoConfig,
382
+ options?: ExpoIapPluginCommonOptions,
383
+ ): boolean {
384
+ const rules = MODULE_RULES[key];
385
+ const explicit = rules.when[selection];
386
+ if (explicit !== undefined) {
387
+ return explicit;
388
+ }
389
+ const override = options?.modules?.[key];
390
+ if (override !== undefined) {
391
+ return override;
392
+ }
393
+ return rules.default({config, options});
185
394
  }
186
395
 
187
- const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
396
+ const withIAP: ConfigPlugin<ExpoIapPluginCommonOptions | void> = (
188
397
  config,
189
398
  options,
190
399
  ) => {
191
400
  try {
401
+ const {includeExpoIap, includeOnside} = resolveModuleSelection(
402
+ config as ExpoConfig,
403
+ options,
404
+ );
405
+
406
+ const autolinkState: AutolinkState = {
407
+ expoIap: includeExpoIap,
408
+ onside: includeOnside,
409
+ };
410
+
411
+ if (includeOnside) {
412
+ config.ios = {
413
+ ...config.ios,
414
+ onside: {
415
+ ...(config.ios?.onside ?? {}),
416
+ enabled: true,
417
+ },
418
+ } as typeof config.ios;
419
+ } else if (config.ios?.onside?.enabled) {
420
+ config.ios.onside.enabled = false;
421
+ }
422
+
192
423
  // Respect explicit flag; fall back to presence of localPath only when flag is unset
193
424
  const isLocalDev = options?.enableLocalDev ?? !!options?.localPath;
194
- // Apply Android modifications (skip adding deps when linking local module)
195
- let result = withIapAndroid(config, {addDeps: !isLocalDev});
425
+ const shouldConfigureAndroid = includeExpoIap;
426
+ const shouldAddAndroidDeps = includeExpoIap && !isLocalDev;
427
+
428
+ // Apply Android modifications (skip when Expo IAP disabled)
429
+ let result = shouldConfigureAndroid
430
+ ? withIapAndroid(config, {addDeps: shouldAddAndroidDeps})
431
+ : config;
196
432
 
197
433
  // iOS: choose one path to avoid overlap
198
434
  if (isLocalDev) {
@@ -222,10 +458,14 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
222
458
  }
223
459
  } else {
224
460
  // Ensure iOS Podfile is set up to resolve public CocoaPods specs
225
- result = withIapIOS(result);
226
- logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
461
+ result = withIapIOS(result, {enableOnside: includeOnside});
462
+ if (includeExpoIap) {
463
+ logOnce('📦 [expo-iap] Using OpenIAP from CocoaPods');
464
+ }
227
465
  }
228
466
 
467
+ syncAutolinking(autolinkState);
468
+
229
469
  return result;
230
470
  } catch (error) {
231
471
  WarningAggregator.addWarningAndroid(
@@ -237,4 +477,4 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
237
477
  }
238
478
  };
239
479
 
240
- export default createRunOncePlugin(withIap, pkg.name, pkg.version);
480
+ export default createRunOncePlugin(withIAP, pkg.name, pkg.version);
@@ -2,7 +2,8 @@
2
2
  "extends": "expo-module-scripts/tsconfig.plugin",
3
3
  "compilerOptions": {
4
4
  "outDir": "build",
5
- "rootDir": "src"
5
+ "rootDir": "src",
6
+ "isolatedModules": true
6
7
  },
7
8
  "include": ["./src"],
8
9
  "exclude": ["**/__mocks__/*", "**/__tests__/*"]
@@ -1 +1 @@
1
- {"root":["./src/withIAP.ts","./src/withLocalOpenIAP.ts"],"version":"5.9.2"}
1
+ {"root":["./src/expoconfig.augmentation.d.ts","./src/withiap.ts","./src/withlocalopeniap.ts"],"version":"5.9.2"}
@@ -1,10 +1,51 @@
1
- import {requireNativeModule} from 'expo-modules-core';
1
+ import {requireNativeModule, UnavailabilityError} from 'expo-modules-core';
2
2
 
3
- // It loads the native module object from the JSI or falls back to
4
- // the bridge module (from NativeModulesProxy) if the remote debugger is on.
5
- const ExpoIapModule = requireNativeModule('ExpoIap');
3
+ type NativeIapModuleName = 'ExpoIapOnside' | 'ExpoIap';
4
+
5
+ const {module: ExpoIapModule, name: resolvedNativeModuleName} =
6
+ resolveNativeModule();
7
+
8
+ export const USING_ONSIDE_SDK = resolvedNativeModuleName === 'ExpoIapOnside';
6
9
 
7
10
  // Platform-specific error codes from native modules
8
11
  export const NATIVE_ERROR_CODES = ExpoIapModule.ERROR_CODES || {};
9
12
 
10
13
  export default ExpoIapModule;
14
+
15
+ function resolveNativeModule(): {
16
+ module: any;
17
+ name: NativeIapModuleName;
18
+ } {
19
+ const candidates: NativeIapModuleName[] = ['ExpoIapOnside', 'ExpoIap'];
20
+
21
+ for (const name of candidates) {
22
+ try {
23
+ const module = requireNativeModule(name);
24
+ return {module, name};
25
+ } catch (error) {
26
+ if (name === 'ExpoIapOnside' && isMissingModuleError(error, name)) {
27
+ // Onside module is optional. If unavailable, fall back to ExpoIap.
28
+ continue;
29
+ }
30
+
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ throw new UnavailabilityError(
36
+ 'expo-iap',
37
+ 'ExpoIap native module is unavailable',
38
+ );
39
+ }
40
+
41
+ function isMissingModuleError(error: unknown, moduleName: string): boolean {
42
+ if (error instanceof UnavailabilityError) {
43
+ return true;
44
+ }
45
+
46
+ if (error instanceof Error) {
47
+ return error.message.includes(`Cannot find native module '${moduleName}'`);
48
+ }
49
+
50
+ return false;
51
+ }