expo-iap 3.1.21 → 3.1.22

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.
@@ -69,9 +69,16 @@ class ExpoIapModule : Module() {
69
69
  scope.launch {
70
70
  connectionMutex.withLock {
71
71
  try {
72
- // Activity may be unavailable in headless/background scenarios.
73
- runCatching { openIap.setActivity(currentActivity) }
74
- .onFailure { Log.w(TAG, "initConnection: Activity missing; proceeding headless", it) }
72
+ // CRITICAL: Set Activity BEFORE calling initConnection
73
+ // Horizon SDK needs Activity to initialize OVRPlatform with proper returnComponent
74
+ // https://github.com/meta-quest/Meta-Spatial-SDK-Samples/issues/82#issuecomment-3452577530
75
+ runCatching { currentActivity }
76
+ .onSuccess {
77
+ ExpoIapLog.debug("Activity available: ${it.javaClass.name}")
78
+ openIap.setActivity(it)
79
+ }.onFailure {
80
+ ExpoIapLog.warning("Activity not available during initConnection - OpenIAP will use Context")
81
+ }
75
82
 
76
83
  // If already connected, short-circuit
77
84
  if (connectionReady.get()) {
@@ -284,7 +291,8 @@ class ExpoIapModule : Module() {
284
291
  ExpoIapHelper.addPurchasePromise(promise)
285
292
  scope.launch {
286
293
  try {
287
- openIap.setActivity(currentActivity)
294
+ val activity = currentActivity
295
+ openIap.setActivity(activity)
288
296
  val result = openIap.requestPurchase(requestProps)
289
297
  val purchases =
290
298
  when (result) {
package/bun.lockb CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <coverage generated="1761243502551" clover="3.2.0">
3
- <project timestamp="1761243502551" name="All files">
2
+ <coverage generated="1761624577860" clover="3.2.0">
3
+ <project timestamp="1761624577861" name="All files">
4
4
  <metrics statements="457" coveredstatements="429" conditionals="251" coveredconditionals="217" methods="95" coveredmethods="75" elements="803" coveredelements="721" complexity="0" loc="457" ncloc="457" packages="3" files="5" classes="5"/>
5
5
  <package name="src">
6
6
  <metrics statements="196" coveredstatements="190" conditionals="99" coveredconditionals="89" methods="41" coveredmethods="32"/>
@@ -131,7 +131,7 @@
131
131
  <div class='footer quiet pad2 space-top1 center small'>
132
132
  Code coverage generated by
133
133
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
134
- at 2025-10-23T18:18:22.536Z
134
+ at 2025-10-28T04:09:37.843Z
135
135
  </div>
136
136
  <script src="prettify.js"></script>
137
137
  <script>
@@ -101,7 +101,7 @@
101
101
  <div class='footer quiet pad2 space-top1 center small'>
102
102
  Code coverage generated by
103
103
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
104
- at 2025-10-23T18:18:22.536Z
104
+ at 2025-10-28T04:09:37.843Z
105
105
  </div>
106
106
  <script src="../prettify.js"></script>
107
107
  <script>
@@ -2239,7 +2239,7 @@ export {<span class="fstat-no" title="function not covered" >ExpoIapConsole}</sp
2239
2239
  <div class='footer quiet pad2 space-top1 center small'>
2240
2240
  Code coverage generated by
2241
2241
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
2242
- at 2025-10-23T18:18:22.536Z
2242
+ at 2025-10-28T04:09:37.843Z
2243
2243
  </div>
2244
2244
  <script src="../prettify.js"></script>
2245
2245
  <script>
@@ -814,7 +814,7 @@ export const createAlternativeBillingTokenAndroid: MutationField&lt;
814
814
  <div class='footer quiet pad2 space-top1 center small'>
815
815
  Code coverage generated by
816
816
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
817
- at 2025-10-23T18:18:22.536Z
817
+ at 2025-10-28T04:09:37.843Z
818
818
  </div>
819
819
  <script src="../../prettify.js"></script>
820
820
  <script>
@@ -116,7 +116,7 @@
116
116
  <div class='footer quiet pad2 space-top1 center small'>
117
117
  Code coverage generated by
118
118
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
119
- at 2025-10-23T18:18:22.536Z
119
+ at 2025-10-28T04:09:37.843Z
120
120
  </div>
121
121
  <script src="../../prettify.js"></script>
122
122
  <script>
@@ -1267,7 +1267,7 @@ export const presentExternalPurchaseLinkIOS: MutationField&lt;
1267
1267
  <div class='footer quiet pad2 space-top1 center small'>
1268
1268
  Code coverage generated by
1269
1269
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
1270
- at 2025-10-23T18:18:22.536Z
1270
+ at 2025-10-28T04:09:37.843Z
1271
1271
  </div>
1272
1272
  <script src="../../prettify.js"></script>
1273
1273
  <script>
@@ -268,7 +268,7 @@ export const ExpoIapConsole = createConsole();
268
268
  <div class='footer quiet pad2 space-top1 center small'>
269
269
  Code coverage generated by
270
270
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
271
- at 2025-10-23T18:18:22.536Z
271
+ at 2025-10-28T04:09:37.843Z
272
272
  </div>
273
273
  <script src="../../prettify.js"></script>
274
274
  <script>
@@ -1111,7 +1111,7 @@ export function getUserFriendlyErrorMessage(error: ErrorLike): string {
1111
1111
  <div class='footer quiet pad2 space-top1 center small'>
1112
1112
  Code coverage generated by
1113
1113
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
1114
- at 2025-10-23T18:18:22.536Z
1114
+ at 2025-10-28T04:09:37.843Z
1115
1115
  </div>
1116
1116
  <script src="../../prettify.js"></script>
1117
1117
  <script>
@@ -116,7 +116,7 @@
116
116
  <div class='footer quiet pad2 space-top1 center small'>
117
117
  Code coverage generated by
118
118
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
119
- at 2025-10-23T18:18:22.536Z
119
+ at 2025-10-28T04:09:37.843Z
120
120
  </div>
121
121
  <script src="../../prettify.js"></script>
122
122
  <script>
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "apple": "1.2.30",
3
- "google": "1.3.3",
3
+ "google": "1.3.4",
4
4
  "gql": "1.2.3"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "3.1.21",
3
+ "version": "3.1.22",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -72,19 +72,23 @@ const modifyAppBuildGradle = (gradle, language, isHorizonEnabled) => {
72
72
  let modified = gradle;
73
73
  // Determine which flavor to use based on isHorizonEnabled
74
74
  const flavor = isHorizonEnabled ? 'horizon' : 'play';
75
+ // Use openiap-google-horizon artifact when horizon is enabled
76
+ const artifactId = isHorizonEnabled
77
+ ? 'openiap-google-horizon'
78
+ : 'openiap-google';
75
79
  // Ensure OpenIAP dependency exists at desired version in app-level build.gradle(.kts)
76
80
  const impl = (ga, v) => language === 'kotlin'
77
81
  ? ` implementation("${ga}:${v}")`
78
82
  : ` implementation "${ga}:${v}"`;
79
- const openiapDep = impl('io.github.hyochan.openiap:openiap-google', OPENIAP_ANDROID_VERSION);
80
- // Remove any existing openiap-google lines (any version, groovy/kotlin, implementation/api)
81
- const openiapAnyLine = /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*\)?\s*$/gm;
83
+ const openiapDep = impl(`io.github.hyochan.openiap:${artifactId}`, OPENIAP_ANDROID_VERSION);
84
+ // Remove any existing openiap-google or openiap-google-horizon lines (any version, groovy/kotlin, implementation/api)
85
+ const openiapAnyLine = /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google(?:-horizon)?:[^"']+["']\s*\)?\s*$/gm;
82
86
  const hadExisting = openiapAnyLine.test(modified);
83
87
  if (hadExisting) {
84
88
  modified = modified.replace(openiapAnyLine, '').replace(/\n{3,}/g, '\n\n');
85
89
  }
86
90
  // Ensure the desired dependency line is present
87
- if (!new RegExp(String.raw `io\.github\.hyochan\.openiap:openiap-google:${OPENIAP_ANDROID_VERSION}`).test(modified)) {
91
+ if (!new RegExp(String.raw `io\.github\.hyochan\.openiap:${artifactId}:${OPENIAP_ANDROID_VERSION}`).test(modified)) {
88
92
  // Insert just after the opening `dependencies {` line
89
93
  modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 1);
90
94
  logOnce(hadExisting
@@ -124,6 +128,20 @@ const withIapAndroid = (config, props) => {
124
128
  return config;
125
129
  });
126
130
  }
131
+ // Set horizonEnabled property in gradle.properties so expo-iap module can pick it up
132
+ config = (0, config_plugins_1.withGradleProperties)(config, (config) => {
133
+ const horizonValue = props?.isHorizonEnabled ?? false;
134
+ // Remove any existing horizonEnabled entries
135
+ config.modResults = config.modResults.filter((item) => item.type !== 'property' || item.key !== 'horizonEnabled');
136
+ // Add the horizonEnabled property
137
+ config.modResults.push({
138
+ type: 'property',
139
+ key: 'horizonEnabled',
140
+ value: String(horizonValue),
141
+ });
142
+ logOnce(`✅ Set horizonEnabled=${horizonValue} in gradle.properties`);
143
+ return config;
144
+ });
127
145
  // Note: missingDimensionStrategy for local dev is handled in withLocalOpenIAP
128
146
  config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
129
147
  const manifest = config.modResults;
@@ -153,20 +171,22 @@ const withIapAndroid = (config, props) => {
153
171
  application['meta-data'] = [];
154
172
  }
155
173
  const metaData = application['meta-data'];
174
+ // Use the correct meta-data name for Horizon Platform SDK
175
+ const horizonMetaDataName = 'com.meta.horizon.platform.ovr.OCULUS_APP_ID';
156
176
  const horizonAppIdMeta = {
157
177
  $: {
158
- 'android:name': 'com.oculus.vr.APP_ID',
178
+ 'android:name': horizonMetaDataName,
159
179
  'android:value': props.horizonAppId,
160
180
  },
161
181
  };
162
- const existingIndex = metaData.findIndex((m) => m.$['android:name'] === 'com.oculus.vr.APP_ID');
182
+ const existingIndex = metaData.findIndex((m) => m.$['android:name'] === horizonMetaDataName);
163
183
  if (existingIndex !== -1) {
164
184
  metaData[existingIndex] = horizonAppIdMeta;
165
- logOnce(`✅ Updated com.oculus.vr.APP_ID to ${props.horizonAppId} in AndroidManifest.xml`);
185
+ logOnce(`✅ Updated ${horizonMetaDataName} to ${props.horizonAppId} in AndroidManifest.xml`);
166
186
  }
167
187
  else {
168
188
  metaData.push(horizonAppIdMeta);
169
- logOnce(`✅ Added com.oculus.vr.APP_ID: ${props.horizonAppId} to AndroidManifest.xml`);
189
+ logOnce(`✅ Added ${horizonMetaDataName}: ${props.horizonAppId} to AndroidManifest.xml`);
170
190
  }
171
191
  }
172
192
  return config;
@@ -179,31 +199,23 @@ const withIapIOS = (config, options) => {
179
199
  if (options) {
180
200
  config = (0, withIosAlternativeBilling_1.withIosAlternativeBilling)(config, options);
181
201
  }
182
- return (0, config_plugins_1.withDangerousMod)(config, [
183
- 'ios',
184
- async (config) => {
185
- const { platformProjectRoot } = config.modRequest;
186
- const podfilePath = path.join(platformProjectRoot, 'Podfile');
187
- if (!fs.existsSync(podfilePath)) {
188
- return config;
189
- }
190
- let content = fs.readFileSync(podfilePath, 'utf8');
191
- // 1) Ensure CocoaPods CDN source is present at the very top
192
- const cdnLine = `source 'https://cdn.cocoapods.org/'`;
193
- if (!content.includes(cdnLine)) {
194
- content = `${cdnLine}\n\n${content}`;
195
- logOnce('📦 expo-iap: Added CocoaPods CDN source to Podfile');
196
- }
197
- // 2) Remove any lingering local OpenIAP pod injection
198
- const localPodRegex = /^\s*pod\s+'openiap'\s*,\s*:path\s*=>\s*['"][^'"]+['"][^\n]*$/gm;
199
- if (localPodRegex.test(content)) {
200
- content = content.replace(localPodRegex, '').replace(/\n{3,}/g, '\n\n');
201
- logOnce('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
202
- }
203
- fs.writeFileSync(podfilePath, content);
204
- return config;
205
- },
206
- ]);
202
+ return (0, config_plugins_1.withPodfile)(config, (config) => {
203
+ let content = config.modResults.contents;
204
+ // 1) Ensure CocoaPods CDN source is present at the very top
205
+ const cdnLine = `source 'https://cdn.cocoapods.org/'`;
206
+ if (!content.includes(cdnLine)) {
207
+ content = `${cdnLine}\n\n${content}`;
208
+ logOnce('📦 expo-iap: Added CocoaPods CDN source to Podfile');
209
+ }
210
+ // 2) Remove any lingering local OpenIAP pod injection
211
+ const localPodRegex = /^\s*pod\s+'openiap'\s*,\s*:path\s*=>\s*['"][^'"]+['"][^\n]*$/gm;
212
+ if (localPodRegex.test(content)) {
213
+ content = content.replace(localPodRegex, '').replace(/\n{3,}/g, '\n\n');
214
+ logOnce('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
215
+ }
216
+ config.modResults.contents = content;
217
+ return config;
218
+ });
207
219
  };
208
220
  const withIap = (config, options) => {
209
221
  try {
@@ -70,7 +70,7 @@ const withLocalOpenIAP = (config, props) => {
70
70
  }
71
71
  return null;
72
72
  };
73
- // iOS: inject local pod path
73
+ // iOS: inject local pod path with wrapper podspec
74
74
  config = (0, config_plugins_1.withDangerousMod)(config, [
75
75
  'ios',
76
76
  async (config) => {
@@ -88,18 +88,23 @@ const withLocalOpenIAP = (config, props) => {
88
88
  console.warn(`⚠️ Podfile not found at ${podfilePath}. Skipping.`);
89
89
  return config;
90
90
  }
91
+ logOnce(`✅ Using local OpenIAP from: ${iosPath}`);
91
92
  let podfileContent = fs.readFileSync(podfilePath, 'utf8');
93
+ // Check if local OpenIAP pod is already configured
92
94
  if (podfileContent.includes("pod 'openiap',")) {
93
95
  logOnce('✅ Local OpenIAP pod already configured');
94
96
  return config;
95
97
  }
96
98
  const targetRegex = /target\s+['"][\w]+['"]\s+do\s*\n\s*use_expo_modules!/;
99
+ const relativePath = path
100
+ .relative(platformProjectRoot, iosPath)
101
+ .replace(/\\/g, '/');
97
102
  if (targetRegex.test(podfileContent)) {
98
103
  podfileContent = podfileContent.replace(targetRegex, (match) => {
99
104
  return `${match}
100
-
105
+
101
106
  # Local OpenIAP pod for development (added by expo-iap plugin)
102
- pod 'openiap', :path => '${iosPath}'`;
107
+ pod 'openiap', :path => '${relativePath}'`;
103
108
  });
104
109
  fs.writeFileSync(podfilePath, podfileContent);
105
110
  logOnce(`✅ Added local OpenIAP pod at: ${iosPath}`);
@@ -195,11 +200,12 @@ const withLocalOpenIAP = (config, props) => {
195
200
  const flavor = props?.isHorizonEnabled ? 'horizon' : 'play';
196
201
  const strategyLine = ` missingDimensionStrategy "platform", "${flavor}"`;
197
202
  let contents = gradle.contents;
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;
203
+ // Remove Maven deps (both openiap-google and openiap-google-horizon)
204
+ // to avoid duplicate classes with local module
205
+ const mavenPattern = /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google(?:-horizon)?:[^"']+["']\s*\)?\s*$/gm;
200
206
  if (mavenPattern.test(contents)) {
201
207
  contents = contents.replace(mavenPattern, '\n');
202
- logOnce('🧹 Removed Maven openiap-google (using local module)');
208
+ logOnce('🧹 Removed Maven openiap-google* dependencies (using local module)');
203
209
  }
204
210
  // Add missingDimensionStrategy (required for flavored module)
205
211
  // Remove any existing platform strategies first to avoid duplicates
@@ -4,7 +4,8 @@ import {
4
4
  WarningAggregator,
5
5
  withAndroidManifest,
6
6
  withAppBuildGradle,
7
- withDangerousMod,
7
+ withGradleProperties,
8
+ withPodfile,
8
9
  } from 'expo/config-plugins';
9
10
  import * as fs from 'fs';
10
11
  import * as path from 'path';
@@ -64,19 +65,24 @@ const modifyAppBuildGradle = (
64
65
  // Determine which flavor to use based on isHorizonEnabled
65
66
  const flavor = isHorizonEnabled ? 'horizon' : 'play';
66
67
 
68
+ // Use openiap-google-horizon artifact when horizon is enabled
69
+ const artifactId = isHorizonEnabled
70
+ ? 'openiap-google-horizon'
71
+ : 'openiap-google';
72
+
67
73
  // Ensure OpenIAP dependency exists at desired version in app-level build.gradle(.kts)
68
74
  const impl = (ga: string, v: string) =>
69
75
  language === 'kotlin'
70
76
  ? ` implementation("${ga}:${v}")`
71
77
  : ` implementation "${ga}:${v}"`;
72
78
  const openiapDep = impl(
73
- 'io.github.hyochan.openiap:openiap-google',
79
+ `io.github.hyochan.openiap:${artifactId}`,
74
80
  OPENIAP_ANDROID_VERSION,
75
81
  );
76
82
 
77
- // Remove any existing openiap-google lines (any version, groovy/kotlin, implementation/api)
83
+ // Remove any existing openiap-google or openiap-google-horizon lines (any version, groovy/kotlin, implementation/api)
78
84
  const openiapAnyLine =
79
- /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*\)?\s*$/gm;
85
+ /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google(?:-horizon)?:[^"']+["']\s*\)?\s*$/gm;
80
86
  const hadExisting = openiapAnyLine.test(modified);
81
87
  if (hadExisting) {
82
88
  modified = modified.replace(openiapAnyLine, '').replace(/\n{3,}/g, '\n\n');
@@ -85,7 +91,7 @@ const modifyAppBuildGradle = (
85
91
  // Ensure the desired dependency line is present
86
92
  if (
87
93
  !new RegExp(
88
- String.raw`io\.github\.hyochan\.openiap:openiap-google:${OPENIAP_ANDROID_VERSION}`,
94
+ String.raw`io\.github\.hyochan\.openiap:${artifactId}:${OPENIAP_ANDROID_VERSION}`,
89
95
  ).test(modified)
90
96
  ) {
91
97
  // Insert just after the opening `dependencies {` line
@@ -155,6 +161,27 @@ const withIapAndroid: ConfigPlugin<
155
161
  });
156
162
  }
157
163
 
164
+ // Set horizonEnabled property in gradle.properties so expo-iap module can pick it up
165
+ config = withGradleProperties(config, (config) => {
166
+ const horizonValue = props?.isHorizonEnabled ?? false;
167
+
168
+ // Remove any existing horizonEnabled entries
169
+ config.modResults = config.modResults.filter(
170
+ (item) => item.type !== 'property' || item.key !== 'horizonEnabled',
171
+ );
172
+
173
+ // Add the horizonEnabled property
174
+ config.modResults.push({
175
+ type: 'property',
176
+ key: 'horizonEnabled',
177
+ value: String(horizonValue),
178
+ });
179
+
180
+ logOnce(`✅ Set horizonEnabled=${horizonValue} in gradle.properties`);
181
+
182
+ return config;
183
+ });
184
+
158
185
  // Note: missingDimensionStrategy for local dev is handled in withLocalOpenIAP
159
186
 
160
187
  config = withAndroidManifest(config, (config) => {
@@ -195,26 +222,29 @@ const withIapAndroid: ConfigPlugin<
195
222
  }
196
223
 
197
224
  const metaData = application['meta-data'];
225
+
226
+ // Use the correct meta-data name for Horizon Platform SDK
227
+ const horizonMetaDataName = 'com.meta.horizon.platform.ovr.OCULUS_APP_ID';
198
228
  const horizonAppIdMeta = {
199
229
  $: {
200
- 'android:name': 'com.oculus.vr.APP_ID',
230
+ 'android:name': horizonMetaDataName,
201
231
  'android:value': props.horizonAppId,
202
232
  },
203
233
  };
204
234
 
205
235
  const existingIndex = metaData.findIndex(
206
- (m) => m.$['android:name'] === 'com.oculus.vr.APP_ID',
236
+ (m) => m.$['android:name'] === horizonMetaDataName,
207
237
  );
208
238
 
209
239
  if (existingIndex !== -1) {
210
240
  metaData[existingIndex] = horizonAppIdMeta;
211
241
  logOnce(
212
- `✅ Updated com.oculus.vr.APP_ID to ${props.horizonAppId} in AndroidManifest.xml`,
242
+ `✅ Updated ${horizonMetaDataName} to ${props.horizonAppId} in AndroidManifest.xml`,
213
243
  );
214
244
  } else {
215
245
  metaData.push(horizonAppIdMeta);
216
246
  logOnce(
217
- `✅ Added com.oculus.vr.APP_ID: ${props.horizonAppId} to AndroidManifest.xml`,
247
+ `✅ Added ${horizonMetaDataName}: ${props.horizonAppId} to AndroidManifest.xml`,
218
248
  );
219
249
  }
220
250
  }
@@ -235,37 +265,27 @@ const withIapIOS: ConfigPlugin<IOSAlternativeBillingConfig | undefined> = (
235
265
  config = withIosAlternativeBilling(config, options);
236
266
  }
237
267
 
238
- return withDangerousMod(config, [
239
- 'ios',
240
- async (config) => {
241
- const {platformProjectRoot} = config.modRequest;
242
- const podfilePath = path.join(platformProjectRoot, 'Podfile');
268
+ return withPodfile(config, (config) => {
269
+ let content = config.modResults.contents;
243
270
 
244
- if (!fs.existsSync(podfilePath)) {
245
- return config;
246
- }
247
-
248
- let content = fs.readFileSync(podfilePath, 'utf8');
249
-
250
- // 1) Ensure CocoaPods CDN source is present at the very top
251
- const cdnLine = `source 'https://cdn.cocoapods.org/'`;
252
- if (!content.includes(cdnLine)) {
253
- content = `${cdnLine}\n\n${content}`;
254
- logOnce('📦 expo-iap: Added CocoaPods CDN source to Podfile');
255
- }
271
+ // 1) Ensure CocoaPods CDN source is present at the very top
272
+ const cdnLine = `source 'https://cdn.cocoapods.org/'`;
273
+ if (!content.includes(cdnLine)) {
274
+ content = `${cdnLine}\n\n${content}`;
275
+ logOnce('📦 expo-iap: Added CocoaPods CDN source to Podfile');
276
+ }
256
277
 
257
- // 2) Remove any lingering local OpenIAP pod injection
258
- const localPodRegex =
259
- /^\s*pod\s+'openiap'\s*,\s*:path\s*=>\s*['"][^'"]+['"][^\n]*$/gm;
260
- if (localPodRegex.test(content)) {
261
- content = content.replace(localPodRegex, '').replace(/\n{3,}/g, '\n\n');
262
- logOnce('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
263
- }
278
+ // 2) Remove any lingering local OpenIAP pod injection
279
+ const localPodRegex =
280
+ /^\s*pod\s+'openiap'\s*,\s*:path\s*=>\s*['"][^'"]+['"][^\n]*$/gm;
281
+ if (localPodRegex.test(content)) {
282
+ content = content.replace(localPodRegex, '').replace(/\n{3,}/g, '\n\n');
283
+ logOnce('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
284
+ }
264
285
 
265
- fs.writeFileSync(podfilePath, content);
266
- return config;
267
- },
268
- ]);
286
+ config.modResults.contents = content;
287
+ return config;
288
+ });
269
289
  };
270
290
 
271
291
  export interface ExpoIapPluginOptions {
@@ -61,7 +61,7 @@ const withLocalOpenIAP: ConfigPlugin<
61
61
  return null;
62
62
  };
63
63
 
64
- // iOS: inject local pod path
64
+ // iOS: inject local pod path with wrapper podspec
65
65
  config = withDangerousMod(config, [
66
66
  'ios',
67
67
  async (config) => {
@@ -82,8 +82,12 @@ const withLocalOpenIAP: ConfigPlugin<
82
82
  console.warn(`⚠️ Podfile not found at ${podfilePath}. Skipping.`);
83
83
  return config;
84
84
  }
85
+
86
+ logOnce(`✅ Using local OpenIAP from: ${iosPath}`);
87
+
85
88
  let podfileContent = fs.readFileSync(podfilePath, 'utf8');
86
89
 
90
+ // Check if local OpenIAP pod is already configured
87
91
  if (podfileContent.includes("pod 'openiap',")) {
88
92
  logOnce('✅ Local OpenIAP pod already configured');
89
93
  return config;
@@ -91,13 +95,16 @@ const withLocalOpenIAP: ConfigPlugin<
91
95
 
92
96
  const targetRegex =
93
97
  /target\s+['"][\w]+['"]\s+do\s*\n\s*use_expo_modules!/;
98
+ const relativePath = path
99
+ .relative(platformProjectRoot, iosPath)
100
+ .replace(/\\/g, '/');
94
101
 
95
102
  if (targetRegex.test(podfileContent)) {
96
103
  podfileContent = podfileContent.replace(targetRegex, (match) => {
97
104
  return `${match}
98
-
105
+
99
106
  # Local OpenIAP pod for development (added by expo-iap plugin)
100
- pod 'openiap', :path => '${iosPath}'`;
107
+ pod 'openiap', :path => '${relativePath}'`;
101
108
  });
102
109
  fs.writeFileSync(podfilePath, podfileContent);
103
110
  logOnce(`✅ Added local OpenIAP pod at: ${iosPath}`);
@@ -224,12 +231,15 @@ const withLocalOpenIAP: ConfigPlugin<
224
231
 
225
232
  let contents = gradle.contents;
226
233
 
227
- // Remove Maven deps (avoid duplicate classes with local module)
234
+ // Remove Maven deps (both openiap-google and openiap-google-horizon)
235
+ // to avoid duplicate classes with local module
228
236
  const mavenPattern =
229
- /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google:[^"']+["']\s*\)?\s*$/gm;
237
+ /^\s*(?:implementation|api)\s*\(?\s*["']io\.github\.hyochan\.openiap:openiap-google(?:-horizon)?:[^"']+["']\s*\)?\s*$/gm;
230
238
  if (mavenPattern.test(contents)) {
231
239
  contents = contents.replace(mavenPattern, '\n');
232
- logOnce('🧹 Removed Maven openiap-google (using local module)');
240
+ logOnce(
241
+ '🧹 Removed Maven openiap-google* dependencies (using local module)',
242
+ );
233
243
  }
234
244
 
235
245
  // Add missingDimensionStrategy (required for flavored module)