expo-iap 2.9.7 → 3.0.1

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 (55) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +10 -2
  3. package/android/build.gradle +7 -2
  4. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +195 -650
  5. package/android/src/main/java/expo/modules/iap/PromiseUtils.kt +85 -0
  6. package/build/ExpoIap.types.d.ts +0 -6
  7. package/build/ExpoIap.types.d.ts.map +1 -1
  8. package/build/ExpoIap.types.js.map +1 -1
  9. package/build/helpers/subscription.d.ts.map +1 -1
  10. package/build/helpers/subscription.js +14 -3
  11. package/build/helpers/subscription.js.map +1 -1
  12. package/build/index.d.ts +6 -73
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +21 -154
  15. package/build/index.js.map +1 -1
  16. package/build/modules/android.d.ts +2 -2
  17. package/build/modules/android.d.ts.map +1 -1
  18. package/build/modules/android.js +11 -1
  19. package/build/modules/android.js.map +1 -1
  20. package/build/modules/ios.d.ts +0 -60
  21. package/build/modules/ios.d.ts.map +1 -1
  22. package/build/modules/ios.js +2 -121
  23. package/build/modules/ios.js.map +1 -1
  24. package/build/types/ExpoIapAndroid.types.d.ts +0 -8
  25. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
  26. package/build/types/ExpoIapAndroid.types.js +0 -1
  27. package/build/types/ExpoIapAndroid.types.js.map +1 -1
  28. package/build/types/ExpoIapIOS.types.d.ts +0 -5
  29. package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
  30. package/build/types/ExpoIapIOS.types.js.map +1 -1
  31. package/build/useIAP.d.ts +0 -18
  32. package/build/useIAP.d.ts.map +1 -1
  33. package/build/useIAP.js +1 -18
  34. package/build/useIAP.js.map +1 -1
  35. package/bun.lock +340 -137
  36. package/codecov.yml +17 -21
  37. package/ios/ExpoIapModule.swift +10 -3
  38. package/jest.config.js +5 -9
  39. package/package.json +5 -3
  40. package/plugin/build/withIAP.d.ts +4 -1
  41. package/plugin/build/withIAP.js +48 -28
  42. package/plugin/build/withLocalOpenIAP.d.ts +6 -2
  43. package/plugin/build/withLocalOpenIAP.js +179 -20
  44. package/plugin/src/withIAP.ts +81 -37
  45. package/plugin/src/withLocalOpenIAP.ts +232 -24
  46. package/src/ExpoIap.types.ts +0 -8
  47. package/src/helpers/subscription.ts +14 -3
  48. package/src/index.ts +22 -230
  49. package/src/modules/android.ts +16 -6
  50. package/src/modules/ios.ts +2 -168
  51. package/src/types/ExpoIapAndroid.types.ts +0 -11
  52. package/src/types/ExpoIapIOS.types.ts +0 -5
  53. package/src/useIAP.ts +3 -55
  54. package/android/src/main/java/expo/modules/iap/PlayUtils.kt +0 -178
  55. package/android/src/main/java/expo/modules/iap/Types.kt +0 -98
package/codecov.yml CHANGED
@@ -6,8 +6,8 @@ codecov:
6
6
  coverage:
7
7
  precision: 2
8
8
  round: down
9
- range: "50...100"
10
-
9
+ range: '50...100'
10
+
11
11
  status:
12
12
  project:
13
13
  default:
@@ -18,7 +18,7 @@ coverage:
18
18
  - unittests
19
19
  paths:
20
20
  - src
21
-
21
+
22
22
  example:
23
23
  target: 40%
24
24
  threshold: 10%
@@ -27,7 +27,7 @@ coverage:
27
27
  - example
28
28
  paths:
29
29
  - example
30
-
30
+
31
31
  patch:
32
32
  default:
33
33
  target: 60%
@@ -43,7 +43,7 @@ parsers:
43
43
  macro: no
44
44
 
45
45
  comment:
46
- layout: "reach,diff,flags,files,footer"
46
+ layout: 'reach,diff,flags,files,footer'
47
47
  behavior: default
48
48
  require_changes: false
49
49
  require_base: false
@@ -54,21 +54,17 @@ flags:
54
54
  paths:
55
55
  - src/
56
56
  carryforward: false
57
-
58
- example:
59
- paths:
60
- - example/
61
- carryforward: false
62
57
 
63
58
  ignore:
64
- - "**/__tests__/**"
65
- - "**/*.test.ts"
66
- - "**/*.test.tsx"
67
- - "**/node_modules/**"
68
- - "build/**"
69
- - "android/**"
70
- - "ios/**"
71
- - "docs/**"
72
- - "plugin/**"
73
- - "*.config.js"
74
- - "*.setup.js"
59
+ - '**/__tests__/**'
60
+ - '**/*.test.ts'
61
+ - '**/*.test.tsx'
62
+ - '**/node_modules/**'
63
+ - 'build/**'
64
+ - 'android/**'
65
+ - 'ios/**'
66
+ - 'docs/**'
67
+ - 'plugin/**'
68
+ - '*.config.js'
69
+ - '*.setup.js'
70
+ - 'example/**'
@@ -271,6 +271,13 @@ public class ExpoIapModule: Module {
271
271
  return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
272
272
  }
273
273
 
274
+ // Backward-compatible alias expected by JS layer/tests
275
+ AsyncFunction("getReceiptDataIOS") { () async throws -> String in
276
+ try await ensureConnection()
277
+ logDebug("getReceiptDataIOS called (alias of getReceiptIOS)")
278
+ return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
279
+ }
280
+
274
281
  AsyncFunction("requestReceiptRefreshIOS") { () async throws -> String in
275
282
  try await ensureConnection()
276
283
  logDebug("requestReceiptRefreshIOS called")
@@ -307,11 +314,11 @@ public class ExpoIapModule: Module {
307
314
  return true
308
315
  }
309
316
 
310
- AsyncFunction("showManageSubscriptionsIOS") { () async throws -> Bool in
317
+ AsyncFunction("showManageSubscriptionsIOS") { () async throws -> [[String: Any?]] in
311
318
  try await ensureConnection()
312
319
  logDebug("showManageSubscriptionsIOS called")
313
- let _ = try await OpenIapModule.shared.showManageSubscriptionsIOS()
314
- return true
320
+ let purchases = try await OpenIapModule.shared.showManageSubscriptionsIOS()
321
+ return OpenIapSerialization.purchases(purchases)
315
322
  }
316
323
 
317
324
  AsyncFunction("deepLinkToSubscriptionsIOS") { () async throws in
package/jest.config.js CHANGED
@@ -1,7 +1,6 @@
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
4
  watchman: false,
6
5
  roots: ['<rootDir>/src'],
7
6
  testMatch: [
@@ -34,12 +33,9 @@ module.exports = {
34
33
  '!src/ExpoIapModule.ts',
35
34
  '!src/ExpoIapModule.web.ts',
36
35
  ],
37
- coverageThreshold: {
38
- global: {
39
- branches: 15,
40
- functions: 15,
41
- lines: 15,
42
- statements: 15,
43
- },
44
- },
36
+ coveragePathIgnorePatterns: [
37
+ '<rootDir>/src/useIAP.ts',
38
+ '<rootDir>/src/ExpoIap.types.ts',
39
+ '<rootDir>/src/utils/constants.ts',
40
+ ],
45
41
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "2.9.7",
3
+ "version": "3.0.1",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -11,10 +11,11 @@
11
11
  "lint:eslint": "eslint --fix 'src/**/*.{ts,tsx}' 'plugin/src/**/*.{ts,tsx}'",
12
12
  "lint:prettier": "prettier --write \"**/*.{md,js,jsx,ts,tsx}\"",
13
13
  "lint:tsc": "tsc -p tsconfig.json --noEmit --skipLibCheck",
14
- "lint:ci": "bun run lint:tsc && bun run lint:eslint && bun run lint:prettier",
14
+ "lint:kt": "sh -c 'command -v ktlint >/dev/null 2>&1 && ktlint --format ./android || { echo \"ktlint not installed; skipping\"; exit 0; }'",
15
+ "lint:ci": "bun run lint:tsc && bun run lint:eslint && bun run lint:prettier && bun run lint:kt",
15
16
  "test": "jest",
16
17
  "test:coverage": "jest --coverage",
17
- "prepare": "expo-module prepare",
18
+ "prepare": "expo-module prepare && husky install",
18
19
  "expo-module": "expo-module",
19
20
  "open:ios": "xed example/ios",
20
21
  "open:android": "open -a \"Android Studio\" example/android",
@@ -42,6 +43,7 @@
42
43
  "license": "MIT",
43
44
  "homepage": "https://github.com/hyochan/expo-iap#readme",
44
45
  "devDependencies": {
46
+ "husky": "^9.0.11",
45
47
  "@jest/globals": "^30.0.5",
46
48
  "@types/jest": "^30.0.0",
47
49
  "@types/react": "~19.1.7",
@@ -1,7 +1,10 @@
1
1
  import { ConfigPlugin } from 'expo/config-plugins';
2
2
  export interface ExpoIapPluginOptions {
3
3
  /** Local development path for OpenIAP library */
4
- localPath?: string;
4
+ localPath?: string | {
5
+ ios?: string;
6
+ android?: string;
7
+ };
5
8
  /** Enable local development mode */
6
9
  enableLocalDev?: boolean;
7
10
  }
@@ -55,39 +55,48 @@ const addLineToGradle = (content, anchor, lineToAdd, offset = 1) => {
55
55
  const lines = content.split('\n');
56
56
  const index = lines.findIndex((line) => line.match(anchor));
57
57
  if (index === -1) {
58
- console.warn(`Anchor "${anchor}" not found in build.gradle. Appending to end.`);
59
- lines.push(lineToAdd);
58
+ config_plugins_1.WarningAggregator.addWarningAndroid('expo-iap', `dependencies { ... } block not found; skipping injection: ${lineToAdd.trim()}`);
59
+ return content;
60
60
  }
61
61
  else {
62
62
  lines.splice(index + offset, 0, lineToAdd);
63
63
  }
64
64
  return lines.join('\n');
65
65
  };
66
- const modifyAppBuildGradle = (gradle) => {
66
+ const modifyAppBuildGradle = (gradle, language) => {
67
67
  let modified = gradle;
68
- // Add billing library dependencies to app-level build.gradle
69
- const billingDep = ` implementation "com.android.billingclient:billing-ktx:8.0.0"`;
70
- const gmsDep = ` implementation "com.google.android.gms:play-services-base:18.1.0"`;
71
- let hasAddedDependency = false;
72
- if (!modified.includes(billingDep)) {
73
- modified = addLineToGradle(modified, /dependencies\s*{/, billingDep);
74
- hasAddedDependency = true;
68
+ // Ensure OpenIAP dependency exists at desired version in app-level build.gradle(.kts)
69
+ const impl = (ga, v) => language === 'kotlin'
70
+ ? ` implementation("${ga}:${v}")`
71
+ : ` implementation "${ga}:${v}"`;
72
+ // Pin OpenIAP Google library to 1.1.0
73
+ const openiapDep = impl('io.github.hyochan.openiap:openiap-google', '1.1.0');
74
+ // Remove any existing openiap-google lines (any version, groovy/kotlin, implementation/api)
75
+ const openiapAnyLine = /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*\)?\s*$/gm;
76
+ const hadExisting = openiapAnyLine.test(modified);
77
+ if (hadExisting) {
78
+ modified = modified.replace(openiapAnyLine, '').replace(/\n{3,}/g, '\n\n');
75
79
  }
76
- if (!modified.includes(gmsDep)) {
77
- modified = addLineToGradle(modified, /dependencies\s*{/, gmsDep, 1);
78
- hasAddedDependency = true;
80
+ // Ensure the desired dependency line is present
81
+ if (!new RegExp(String.raw `io\.github\.hyochan\.openiap:openiap-google:1\.1\.0`).test(modified)) {
82
+ // Insert just after the opening `dependencies {` line
83
+ modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 1);
84
+ logOnce(hadExisting
85
+ ? '🛠️ expo-iap: Replaced OpenIAP dependency with 1.1.0'
86
+ : '🛠️ expo-iap: Added OpenIAP dependency (1.1.0) to build.gradle');
79
87
  }
80
- // Log only once and only if we actually added dependencies
81
- if (hasAddedDependency)
82
- logOnce('🛠️ expo-iap: Added billing dependencies to build.gradle');
83
88
  return modified;
84
89
  };
85
- const withIapAndroid = (config) => {
86
- // Add IAP dependencies to app build.gradle
87
- config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
88
- config.modResults.contents = modifyAppBuildGradle(config.modResults.contents);
89
- return config;
90
- });
90
+ const withIapAndroid = (config, props) => {
91
+ const addDeps = props?.addDeps ?? true;
92
+ if (addDeps) {
93
+ config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
94
+ // language provided by config-plugins: 'groovy' | 'kotlin'
95
+ const language = config.modResults.language || 'groovy';
96
+ config.modResults.contents = modifyAppBuildGradle(config.modResults.contents, language);
97
+ return config;
98
+ });
99
+ }
91
100
  config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
92
101
  const manifest = config.modResults;
93
102
  if (!manifest.manifest['uses-permission']) {
@@ -137,17 +146,28 @@ const withIapIOS = (config) => {
137
146
  };
138
147
  const withIap = (config, options) => {
139
148
  try {
140
- // Apply Android modifications
141
- let result = withIapAndroid(config);
149
+ // Respect explicit flag; fall back to presence of localPath only when flag is unset
150
+ const isLocalDev = options?.enableLocalDev ?? !!options?.localPath;
151
+ // Apply Android modifications (skip adding deps when linking local module)
152
+ let result = withIapAndroid(config, { addDeps: !isLocalDev });
142
153
  // iOS: choose one path to avoid overlap
143
- if (options?.enableLocalDev || options?.localPath) {
154
+ if (isLocalDev) {
144
155
  if (!options?.localPath) {
145
156
  config_plugins_1.WarningAggregator.addWarningIOS('expo-iap', 'enableLocalDev is true but no localPath provided. Skipping local OpenIAP integration.');
146
157
  }
147
158
  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 });
159
+ const raw = options.localPath;
160
+ const resolved = typeof raw === 'string'
161
+ ? path.resolve(raw)
162
+ : {
163
+ ios: raw.ios ? path.resolve(raw.ios) : undefined,
164
+ android: raw.android ? path.resolve(raw.android) : undefined,
165
+ };
166
+ const preview = typeof resolved === 'string'
167
+ ? resolved
168
+ : `ios=${resolved.ios ?? 'auto'}, android=${resolved.android ?? 'auto'}`;
169
+ logOnce(`🔧 [expo-iap] Enabling local OpenIAP: ${preview}`);
170
+ result = (0, withLocalOpenIAP_1.default)(result, { localPath: resolved });
151
171
  }
152
172
  }
153
173
  else {
@@ -1,9 +1,13 @@
1
- import { ConfigPlugin } from '@expo/config-plugins';
1
+ import { ConfigPlugin } from 'expo/config-plugins';
2
2
  /**
3
3
  * Plugin to add local OpenIAP pod dependency for development
4
4
  * This is only for local development with openiap-apple library
5
5
  */
6
+ type LocalPathOption = string | {
7
+ ios?: string;
8
+ android?: string;
9
+ };
6
10
  declare const withLocalOpenIAP: ConfigPlugin<{
7
- localPath?: string;
11
+ localPath?: LocalPathOption;
8
12
  } | void>;
9
13
  export default withLocalOpenIAP;
@@ -33,51 +33,61 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- const config_plugins_1 = require("@expo/config-plugins");
36
+ const config_plugins_1 = require("expo/config-plugins");
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- /**
40
- * Plugin to add local OpenIAP pod dependency for development
41
- * This is only for local development with openiap-apple library
42
- */
43
39
  const withLocalOpenIAP = (config, props) => {
44
- return (0, config_plugins_1.withDangerousMod)(config, [
40
+ // Helper to resolve Android module path
41
+ const resolveAndroidModulePath = (p) => {
42
+ if (!p)
43
+ return null;
44
+ // Prefer the module directory if it exists
45
+ const candidates = [
46
+ path.join(p, 'openiap-google'),
47
+ path.join(p, 'openiap'),
48
+ p,
49
+ ];
50
+ for (const c of candidates) {
51
+ if (fs.existsSync(path.join(c, 'build.gradle')) ||
52
+ fs.existsSync(path.join(c, 'build.gradle.kts'))) {
53
+ return c;
54
+ }
55
+ }
56
+ return null;
57
+ };
58
+ // iOS: inject local pod path
59
+ config = (0, config_plugins_1.withDangerousMod)(config, [
45
60
  'ios',
46
61
  async (config) => {
47
- const { platformProjectRoot } = config.modRequest;
62
+ const { platformProjectRoot, projectRoot } = config.modRequest;
63
+ const raw = props?.localPath;
64
+ const iosPath = (typeof raw === 'string' ? raw : raw?.ios) ||
65
+ path.resolve(projectRoot, 'openiap-apple');
48
66
  const podfilePath = path.join(platformProjectRoot, 'Podfile');
49
- // Default local path or use provided one
50
- const localOpenIapPath = props?.localPath ||
51
- path.resolve(config.modRequest.projectRoot, 'openiap-apple');
52
- // Check if local path exists
53
- if (!fs.existsSync(localOpenIapPath)) {
54
- console.warn(`⚠️ Local openiap-apple path not found: ${localOpenIapPath}`);
55
- console.warn(' Skipping local pod injection. Using default pod resolution.');
67
+ if (!fs.existsSync(iosPath)) {
68
+ console.warn(`⚠️ Local openiap-apple path not found: ${iosPath}`);
69
+ console.warn(' Skipping local pod injection.');
56
70
  return config;
57
71
  }
58
- // Read Podfile
59
72
  if (!fs.existsSync(podfilePath)) {
60
73
  console.warn(`⚠️ Podfile not found at ${podfilePath}. Skipping.`);
61
74
  return config;
62
75
  }
63
76
  let podfileContent = fs.readFileSync(podfilePath, 'utf8');
64
- // Check if already has the local pod reference
65
77
  if (podfileContent.includes("pod 'openiap',")) {
66
78
  console.log('✅ Local OpenIAP pod already configured');
67
79
  return config;
68
80
  }
69
- // Find the target block and inject the local pod
70
81
  const targetRegex = /target\s+['"][\w]+['"]\s+do\s*\n\s*use_expo_modules!/;
71
82
  if (targetRegex.test(podfileContent)) {
72
83
  podfileContent = podfileContent.replace(targetRegex, (match) => {
73
84
  return `${match}
74
85
 
75
86
  # Local OpenIAP pod for development (added by expo-iap plugin)
76
- pod 'openiap', :path => '${localOpenIapPath}'`;
87
+ pod 'openiap', :path => '${iosPath}'`;
77
88
  });
78
- // Write back to Podfile
79
89
  fs.writeFileSync(podfilePath, podfileContent);
80
- console.log(`✅ Added local OpenIAP pod at: ${localOpenIapPath}`);
90
+ console.log(`✅ Added local OpenIAP pod at: ${iosPath}`);
81
91
  }
82
92
  else {
83
93
  console.warn('⚠️ Could not find target block in Podfile');
@@ -85,5 +95,154 @@ const withLocalOpenIAP = (config, props) => {
85
95
  return config;
86
96
  },
87
97
  ]);
98
+ // Android: include local module and add dependency if available
99
+ config = (0, config_plugins_1.withSettingsGradle)(config, (config) => {
100
+ const raw = props?.localPath;
101
+ const projectRoot = config.modRequest.projectRoot;
102
+ const androidInput = typeof raw === 'string' ? undefined : raw?.android;
103
+ const androidModulePath = resolveAndroidModulePath(androidInput) ||
104
+ resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) ||
105
+ null;
106
+ if (!androidModulePath || !fs.existsSync(androidModulePath)) {
107
+ if (androidInput) {
108
+ console.warn(`⚠️ Could not resolve Android OpenIAP module at: ${androidInput}. Skipping local Android linkage.`);
109
+ }
110
+ return config;
111
+ }
112
+ // 1) settings.gradle: include and map projectDir
113
+ const settings = config.modResults;
114
+ const includeLine = "include ':openiap-google'";
115
+ const projectDirLine = `project(':openiap-google').projectDir = new File('${androidModulePath.replace(/\\/g, '/')}')`;
116
+ let contents = settings.contents ?? '';
117
+ // Ensure pluginManagement has plugin mappings required by the included module
118
+ const injectPluginManagement = () => {
119
+ const header = 'pluginManagement {';
120
+ const needsVannik = !/id\s*\(\s*["']com\.vanniktech\.maven\.publish["']/.test(contents);
121
+ const needsKotlinAndroid = !/id\s*\(\s*["']org\.jetbrains\.kotlin\.android["']/.test(contents);
122
+ const needsCompose = !/id\s*\(\s*["']org\.jetbrains\.kotlin\.plugin\.compose["']/.test(contents);
123
+ const needsRepos = !/pluginManagement[\s\S]*?repositories\s*\{/.test(contents);
124
+ const pluginLines = [];
125
+ if (needsVannik)
126
+ pluginLines.push(` id("com.vanniktech.maven.publish") version "0.29.0"`);
127
+ if (needsKotlinAndroid)
128
+ pluginLines.push(` id("org.jetbrains.kotlin.android") version "2.0.21"`);
129
+ if (needsCompose)
130
+ pluginLines.push(` id("org.jetbrains.kotlin.plugin.compose") version "2.0.21"`);
131
+ // If everything already present, skip
132
+ if (pluginLines.length === 0 && !needsRepos)
133
+ return;
134
+ const pluginsBlock = pluginLines.length
135
+ ? `plugins {\n${pluginLines.join('\n')}\n}`
136
+ : '';
137
+ const reposBlock = `repositories { gradlePluginPortal(); google(); mavenCentral() }`;
138
+ if (contents.includes(header)) {
139
+ contents = contents.replace(/pluginManagement\s*\{/, (m) => {
140
+ let injection = m + `\n // Added by expo-iap (local openiap-google)\n`;
141
+ if (pluginsBlock)
142
+ injection += ` ${pluginsBlock}\n`;
143
+ if (needsRepos)
144
+ injection += ` ${reposBlock}\n`;
145
+ return injection;
146
+ });
147
+ }
148
+ else {
149
+ contents =
150
+ `pluginManagement {\n // Added by expo-iap (local openiap-google)\n` +
151
+ (pluginsBlock ? ` ${pluginsBlock}\n` : '') +
152
+ ` ${reposBlock}\n}\n\n${contents}`;
153
+ }
154
+ };
155
+ if (!/com\.vanniktech\.maven\.publish/.test(contents) ||
156
+ !/org\.jetbrains\.kotlin\.android/.test(contents)) {
157
+ injectPluginManagement();
158
+ }
159
+ if (!contents.includes(includeLine))
160
+ contents += `\n${includeLine}\n`;
161
+ if (!contents.includes(projectDirLine))
162
+ contents += `${projectDirLine}\n`;
163
+ settings.contents = contents;
164
+ console.log(`✅ Linked local Android module at: ${androidModulePath}`);
165
+ return config;
166
+ });
167
+ // 2) app/build.gradle: add implementation project(':openiap-google')
168
+ config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
169
+ const raw = props?.localPath;
170
+ const projectRoot = config.modRequest.projectRoot;
171
+ const androidInput = typeof raw === 'string' ? undefined : raw?.android;
172
+ const androidModulePath = resolveAndroidModulePath(androidInput) ||
173
+ resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) ||
174
+ null;
175
+ if (!androidModulePath || !fs.existsSync(androidModulePath)) {
176
+ return config;
177
+ }
178
+ const gradle = config.modResults;
179
+ const dependencyLine = ` implementation project(':openiap-google')`;
180
+ // Remove any previously added Maven deps for openiap-google to avoid duplicate classes
181
+ const removalPatterns = [
182
+ // Groovy DSL: implementation "io.github.hyochan.openiap:openiap-google:x.y.z" or api "..."
183
+ /^\s*(?:implementation|api)\s+["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*$/gm,
184
+ // Kotlin DSL: implementation("io.github.hyochan.openiap:openiap-google:x.y.z") or api("...")
185
+ /^\s*(?:implementation|api)\s*\(\s*["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*\)\s*$/gm,
186
+ ];
187
+ let contents = gradle.contents;
188
+ let removedAny = false;
189
+ for (const pattern of removalPatterns) {
190
+ if (pattern.test(contents)) {
191
+ contents = contents.replace(pattern, '\n');
192
+ removedAny = true;
193
+ }
194
+ }
195
+ if (removedAny) {
196
+ gradle.contents = contents;
197
+ console.log('🧹 Removed Maven openiap-google to use local :openiap-google');
198
+ }
199
+ if (!gradle.contents.includes(dependencyLine)) {
200
+ const anchor = /dependencies\s*\{/m;
201
+ if (anchor.test(gradle.contents)) {
202
+ gradle.contents = gradle.contents.replace(anchor, (m) => `${m}\n${dependencyLine}`);
203
+ }
204
+ else {
205
+ gradle.contents += `\n\ndependencies {\n${dependencyLine}\n}\n`;
206
+ }
207
+ console.log('🛠️ Added dependency on local :openiap-google project');
208
+ }
209
+ return config;
210
+ });
211
+ // 3) Ensure final cleanup in app/build.gradle after all mods are applied
212
+ config = (0, config_plugins_1.withDangerousMod)(config, [
213
+ 'android',
214
+ async (config) => {
215
+ try {
216
+ const { platformProjectRoot } = config.modRequest;
217
+ const appBuildGradle = path.join(platformProjectRoot, 'app', 'build.gradle');
218
+ if (fs.existsSync(appBuildGradle)) {
219
+ let contents = fs.readFileSync(appBuildGradle, 'utf8');
220
+ const patterns = [
221
+ // Groovy DSL
222
+ /^\s*(?:implementation|api)\s+["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*$/gm,
223
+ // Kotlin DSL
224
+ /^\s*(?:implementation|api)\s*\(\s*["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*\)\s*$/gm,
225
+ ];
226
+ let changed = false;
227
+ for (const p of patterns) {
228
+ if (p.test(contents)) {
229
+ contents = contents.replace(p, '\n');
230
+ changed = true;
231
+ }
232
+ }
233
+ if (changed) {
234
+ fs.writeFileSync(appBuildGradle, contents);
235
+ console.log('🧹 expo-iap: Cleaned Maven openiap-google for local :openiap-google');
236
+ }
237
+ }
238
+ }
239
+ catch (e) {
240
+ console.warn('expo-iap: cleanup step failed:', e);
241
+ }
242
+ return config;
243
+ },
244
+ ]);
245
+ // (removed) Avoid global root build.gradle mutations; included module should manage its plugins
246
+ return config;
88
247
  };
89
248
  exports.default = withLocalOpenIAP;