expo-iap 2.9.6 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.eslintrc.js +24 -0
  2. package/CHANGELOG.md +43 -0
  3. package/README.md +1 -1
  4. package/android/build.gradle +7 -2
  5. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +195 -668
  6. package/android/src/main/java/expo/modules/iap/PromiseUtils.kt +85 -0
  7. package/build/ExpoIap.types.d.ts +0 -6
  8. package/build/ExpoIap.types.d.ts.map +1 -1
  9. package/build/ExpoIap.types.js.map +1 -1
  10. package/build/helpers/subscription.d.ts.map +1 -1
  11. package/build/helpers/subscription.js +14 -3
  12. package/build/helpers/subscription.js.map +1 -1
  13. package/build/index.d.ts +6 -73
  14. package/build/index.d.ts.map +1 -1
  15. package/build/index.js +21 -154
  16. package/build/index.js.map +1 -1
  17. package/build/modules/android.d.ts +2 -2
  18. package/build/modules/android.d.ts.map +1 -1
  19. package/build/modules/android.js +11 -1
  20. package/build/modules/android.js.map +1 -1
  21. package/build/modules/ios.d.ts +0 -60
  22. package/build/modules/ios.d.ts.map +1 -1
  23. package/build/modules/ios.js +2 -121
  24. package/build/modules/ios.js.map +1 -1
  25. package/build/types/ExpoIapAndroid.types.d.ts +0 -8
  26. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
  27. package/build/types/ExpoIapAndroid.types.js +0 -1
  28. package/build/types/ExpoIapAndroid.types.js.map +1 -1
  29. package/build/types/ExpoIapIOS.types.d.ts +0 -5
  30. package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
  31. package/build/types/ExpoIapIOS.types.js.map +1 -1
  32. package/build/useIAP.d.ts +0 -18
  33. package/build/useIAP.d.ts.map +1 -1
  34. package/build/useIAP.js +1 -18
  35. package/build/useIAP.js.map +1 -1
  36. package/bun.lock +340 -137
  37. package/codecov.yml +17 -21
  38. package/ios/ExpoIapModule.swift +50 -23
  39. package/jest.config.js +5 -9
  40. package/package.json +5 -3
  41. package/plugin/build/withIAP.d.ts +4 -1
  42. package/plugin/build/withIAP.js +38 -24
  43. package/plugin/build/withLocalOpenIAP.d.ts +6 -2
  44. package/plugin/build/withLocalOpenIAP.js +175 -20
  45. package/plugin/src/withIAP.ts +66 -30
  46. package/plugin/src/withLocalOpenIAP.ts +228 -24
  47. package/src/ExpoIap.types.ts +0 -8
  48. package/src/helpers/subscription.ts +14 -3
  49. package/src/index.ts +22 -230
  50. package/src/modules/android.ts +16 -6
  51. package/src/modules/ios.ts +2 -168
  52. package/src/types/ExpoIapAndroid.types.ts +0 -11
  53. package/src/types/ExpoIapIOS.types.ts +0 -5
  54. package/src/useIAP.ts +3 -55
  55. package/android/src/main/java/expo/modules/iap/PlayUtils.kt +0 -178
  56. 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/**'
@@ -22,6 +22,8 @@ struct OpenIapEvent {
22
22
  @available(iOS 15.0, tvOS 15.0, *)
23
23
  @MainActor
24
24
  public class ExpoIapModule: Module {
25
+ // Connection state for local validation parity with RN module
26
+ private var isInitialized: Bool = false
25
27
  // Subscriptions for OpenIapModule event listeners
26
28
  private var purchaseUpdatedSub: Subscription?
27
29
  private var purchaseErrorSub: Subscription?
@@ -65,6 +67,8 @@ public class ExpoIapModule: Module {
65
67
  AsyncFunction("initConnection") { () async throws -> Bool in
66
68
  logDebug("initConnection called")
67
69
  let isConnected = try await OpenIapModule.shared.initConnection()
70
+ // Track initialization locally for ensureConnection()
71
+ await MainActor.run { self.isInitialized = isConnected }
68
72
  logDebug("Connection initialized: \(isConnected)")
69
73
  return isConnected
70
74
  }
@@ -74,12 +78,14 @@ public class ExpoIapModule: Module {
74
78
  let _ = try await OpenIapModule.shared.endConnection()
75
79
 
76
80
  logDebug("Connection ended")
81
+ await MainActor.run { self.isInitialized = false }
77
82
  return true
78
83
  }
79
84
 
80
85
  // MARK: - Product Management
81
86
 
82
87
  AsyncFunction("fetchProducts") { (params: [String: Any]) async throws -> [[String: Any?]] in
88
+ try await ensureConnection()
83
89
  logDebug("fetchProducts raw params: \(params)")
84
90
 
85
91
  // Handle both object format {skus: [...], type: "..."} and array format
@@ -148,6 +154,7 @@ public class ExpoIapModule: Module {
148
154
  guard let sku = params["sku"] as? String, !sku.isEmpty else {
149
155
  throw OpenIapError.make(code: OpenIapError.E_PURCHASE_ERROR, message: "Missing required 'sku'")
150
156
  }
157
+ try await ensureConnection()
151
158
 
152
159
  // Optional fields
153
160
  let andFinish = (params["andDangerouslyFinishTransactionAutomatically"] as? Bool) ?? false
@@ -205,6 +212,7 @@ public class ExpoIapModule: Module {
205
212
  }
206
213
 
207
214
  AsyncFunction("finishTransaction") { (transactionId: String) async throws -> Bool in
215
+ try await ensureConnection()
208
216
  logDebug("finishTransaction called with id: \(transactionId)")
209
217
  let result = try await OpenIapModule.shared.finishTransaction(transactionIdentifier: transactionId)
210
218
  return result
@@ -213,6 +221,7 @@ public class ExpoIapModule: Module {
213
221
  // MARK: - Purchase History
214
222
 
215
223
  AsyncFunction("getAvailablePurchases") { (options: [String: Any?]?) async throws -> [[String: Any?]] in
224
+ try await ensureConnection()
216
225
  logDebug("getAvailablePurchases called")
217
226
 
218
227
  // Build options and get purchases directly from OpenIapModule
@@ -228,6 +237,7 @@ public class ExpoIapModule: Module {
228
237
 
229
238
  // Legacy function for backward compatibility
230
239
  AsyncFunction("getAvailableItems") { (alsoPublishToEventListener: Bool, onlyIncludeActiveItems: Bool) async throws -> [[String: Any?]] in
240
+ try await ensureConnection()
231
241
  logDebug("getAvailableItems called (legacy)")
232
242
 
233
243
  let purchaseOptions = OpenIapGetAvailablePurchasesProps(
@@ -239,6 +249,7 @@ public class ExpoIapModule: Module {
239
249
  }
240
250
 
241
251
  AsyncFunction("getPendingTransactionsIOS") { () async throws -> [[String: Any?]] in
252
+ try await ensureConnection()
242
253
  logDebug("getPendingTransactionsIOS called")
243
254
 
244
255
  let pendingTransactions = try await OpenIapModule.shared.getPendingTransactionsIOS()
@@ -246,6 +257,7 @@ public class ExpoIapModule: Module {
246
257
  }
247
258
 
248
259
  AsyncFunction("clearTransactionIOS") { () async throws -> Bool in
260
+ try await ensureConnection()
249
261
  logDebug("clearTransactionIOS called")
250
262
  try await OpenIapModule.shared.clearTransactionIOS()
251
263
  return true
@@ -254,17 +266,27 @@ public class ExpoIapModule: Module {
254
266
  // MARK: - Receipt & Validation
255
267
 
256
268
  AsyncFunction("getReceiptIOS") { () async throws -> String in
269
+ try await ensureConnection()
257
270
  logDebug("getReceiptIOS called")
258
271
  return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
259
272
  }
260
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
+
261
281
  AsyncFunction("requestReceiptRefreshIOS") { () async throws -> String in
282
+ try await ensureConnection()
262
283
  logDebug("requestReceiptRefreshIOS called")
263
284
  // Receipt refresh is handled automatically by StoreKit 2
264
285
  return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
265
286
  }
266
287
 
267
288
  AsyncFunction("validateReceiptIOS") { (sku: String) async throws -> [String: Any?] in
289
+ try await ensureConnection()
268
290
  logDebug("validateReceiptIOS called for sku: \(sku)")
269
291
  do {
270
292
  // Use OpenIapReceiptValidationProps to keep naming parity with OpenIAP
@@ -286,15 +308,17 @@ public class ExpoIapModule: Module {
286
308
  // MARK: - iOS Specific Features
287
309
 
288
310
  AsyncFunction("presentCodeRedemptionSheetIOS") { () async throws -> Bool in
311
+ try await ensureConnection()
289
312
  logDebug("presentCodeRedemptionSheetIOS called")
290
313
  let _ = try await OpenIapModule.shared.presentCodeRedemptionSheetIOS()
291
314
  return true
292
315
  }
293
316
 
294
- AsyncFunction("showManageSubscriptionsIOS") { () async throws -> Bool in
317
+ AsyncFunction("showManageSubscriptionsIOS") { () async throws -> [[String: Any?]] in
318
+ try await ensureConnection()
295
319
  logDebug("showManageSubscriptionsIOS called")
296
- let _ = try await OpenIapModule.shared.showManageSubscriptionsIOS()
297
- return true
320
+ let purchases = try await OpenIapModule.shared.showManageSubscriptionsIOS()
321
+ return OpenIapSerialization.purchases(purchases)
298
322
  }
299
323
 
300
324
  AsyncFunction("deepLinkToSubscriptionsIOS") { () async throws in
@@ -310,11 +334,13 @@ public class ExpoIapModule: Module {
310
334
  }
311
335
 
312
336
  AsyncFunction("beginRefundRequestIOS") { (sku: String) async throws -> String? in
337
+ try await ensureConnection()
313
338
  logDebug("beginRefundRequestIOS called for sku: \(sku)")
314
339
  return try await OpenIapModule.shared.beginRefundRequestIOS(sku: sku)
315
340
  }
316
341
 
317
342
  AsyncFunction("getPromotedProductIOS") { () async throws -> [String: Any?]? in
343
+ try await ensureConnection()
318
344
  logDebug("getPromotedProductIOS called")
319
345
 
320
346
  if let promoted = try await OpenIapModule.shared.getPromotedProductIOS() {
@@ -327,11 +353,13 @@ public class ExpoIapModule: Module {
327
353
  return nil
328
354
  }
329
355
  AsyncFunction("getStorefrontIOS") { () async throws -> String in
356
+ try await ensureConnection()
330
357
  logDebug("getStorefrontIOS called")
331
358
  return try await OpenIapModule.shared.getStorefrontIOS()
332
359
  }
333
360
 
334
361
  AsyncFunction("syncIOS") { () async throws -> Bool in
362
+ try await ensureConnection()
335
363
  logDebug("syncIOS called")
336
364
  return try await OpenIapModule.shared.syncIOS()
337
365
  }
@@ -339,21 +367,25 @@ public class ExpoIapModule: Module {
339
367
  // MARK: - Additional iOS Methods
340
368
 
341
369
  AsyncFunction("isTransactionVerifiedIOS") { (sku: String) async throws -> Bool in
370
+ try await ensureConnection()
342
371
  logDebug("isTransactionVerifiedIOS called for sku: \(sku)")
343
372
  return await OpenIapModule.shared.isTransactionVerifiedIOS(sku: sku)
344
373
  }
345
374
 
346
375
  AsyncFunction("getTransactionJwsIOS") { (sku: String) async throws -> String? in
376
+ try await ensureConnection()
347
377
  logDebug("getTransactionJwsIOS called for sku: \(sku)")
348
378
  return try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku)
349
379
  }
350
380
 
351
381
  AsyncFunction("isEligibleForIntroOfferIOS") { (groupID: String) async throws -> Bool in
382
+ try await ensureConnection()
352
383
  logDebug("isEligibleForIntroOfferIOS called for groupID: \(groupID)")
353
384
  return await OpenIapModule.shared.isEligibleForIntroOfferIOS(groupID: groupID)
354
385
  }
355
386
 
356
387
  AsyncFunction("subscriptionStatusIOS") { (sku: String) async throws -> [[String: Any?]]? in
388
+ try await ensureConnection()
357
389
  logDebug("subscriptionStatusIOS called for sku: \(sku)")
358
390
 
359
391
  if let statuses = try await OpenIapModule.shared.subscriptionStatusIOS(sku: sku) {
@@ -365,27 +397,9 @@ public class ExpoIapModule: Module {
365
397
  ]
366
398
 
367
399
  if let info = status.renewalInfo {
368
- // Convert autoRenewStatus to a proper boolean for willAutoRenew
369
- let willAutoRenew: Bool = {
370
- // Try boolean first
371
- if let b = info.autoRenewStatus as? Bool { return b }
372
- // Fallback to string normalization
373
- let normalized = String(describing: info.autoRenewStatus).lowercased()
374
- let truthy = Set([
375
- "willrenew",
376
- "will_autorenew",
377
- "will-auto-renew",
378
- "auto_renew_on",
379
- "true",
380
- "1",
381
- "on",
382
- "yes",
383
- ])
384
- return truthy.contains(normalized)
385
- }()
386
-
400
+ // autoRenewStatus is a Bool from OpenIAP types
387
401
  let renewalInfo: [String: Any?] = [
388
- "willAutoRenew": willAutoRenew,
402
+ "willAutoRenew": info.autoRenewStatus,
389
403
  "autoRenewPreference": info.autoRenewPreference
390
404
  ]
391
405
  dict["renewalInfo"] = renewalInfo
@@ -398,6 +412,7 @@ public class ExpoIapModule: Module {
398
412
  }
399
413
 
400
414
  AsyncFunction("currentEntitlementIOS") { (sku: String) async throws -> [String: Any?]? in
415
+ try await ensureConnection()
401
416
  logDebug("currentEntitlementIOS called for sku: \(sku)")
402
417
  do {
403
418
  if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku) {
@@ -410,6 +425,7 @@ public class ExpoIapModule: Module {
410
425
  }
411
426
 
412
427
  AsyncFunction("latestTransactionIOS") { (sku: String) async throws -> [String: Any?]? in
428
+ try await ensureConnection()
413
429
  logDebug("latestTransactionIOS called for sku: \(sku)")
414
430
  do {
415
431
  if let transaction = try await OpenIapModule.shared.latestTransactionIOS(sku: sku) {
@@ -468,4 +484,15 @@ public class ExpoIapModule: Module {
468
484
  _ = try? await OpenIapModule.shared.endConnection()
469
485
  }
470
486
 
487
+ // MARK: - Private Helper Methods
488
+
489
+ private func ensureConnection() throws {
490
+ guard isInitialized else {
491
+ throw OpenIapError.make(
492
+ code: OpenIapError.E_INIT_CONNECTION,
493
+ message: "Connection not initialized. Call initConnection() first."
494
+ )
495
+ }
496
+ }
497
+
471
498
  }
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.6",
3
+ "version": "3.0.0",
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,43 @@ 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"`;
68
+ // Add OpenIAP dependency to 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
+ const hasGA = (ga) => new RegExp(String.raw `\b(?:implementation|api)\s*\(?["']${ga}:`, 'm').test(modified);
71
75
  let hasAddedDependency = false;
72
- if (!modified.includes(billingDep)) {
73
- modified = addLineToGradle(modified, /dependencies\s*{/, billingDep);
74
- hasAddedDependency = true;
75
- }
76
- if (!modified.includes(gmsDep)) {
77
- modified = addLineToGradle(modified, /dependencies\s*{/, gmsDep, 1);
76
+ if (!hasGA('io.github.hyochan.openiap:openiap-google')) {
77
+ modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 0);
78
78
  hasAddedDependency = true;
79
79
  }
80
80
  // Log only once and only if we actually added dependencies
81
81
  if (hasAddedDependency)
82
- logOnce('🛠️ expo-iap: Added billing dependencies to build.gradle');
82
+ logOnce('🛠️ expo-iap: Added OpenIAP dependency to build.gradle');
83
83
  return modified;
84
84
  };
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
- });
85
+ const withIapAndroid = (config, props) => {
86
+ const addDeps = props?.addDeps ?? true;
87
+ if (addDeps) {
88
+ config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
89
+ // language provided by config-plugins: 'groovy' | 'kotlin'
90
+ const language = config.modResults.language || 'groovy';
91
+ config.modResults.contents = modifyAppBuildGradle(config.modResults.contents, language);
92
+ return config;
93
+ });
94
+ }
91
95
  config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
92
96
  const manifest = config.modResults;
93
97
  if (!manifest.manifest['uses-permission']) {
@@ -137,17 +141,27 @@ const withIapIOS = (config) => {
137
141
  };
138
142
  const withIap = (config, options) => {
139
143
  try {
140
- // Apply Android modifications
141
- let result = withIapAndroid(config);
144
+ const isLocalDev = !!(options?.enableLocalDev || options?.localPath);
145
+ // Apply Android modifications (skip adding deps when linking local module)
146
+ let result = withIapAndroid(config, { addDeps: !isLocalDev });
142
147
  // iOS: choose one path to avoid overlap
143
148
  if (options?.enableLocalDev || options?.localPath) {
144
149
  if (!options?.localPath) {
145
150
  config_plugins_1.WarningAggregator.addWarningIOS('expo-iap', 'enableLocalDev is true but no localPath provided. Skipping local OpenIAP integration.');
146
151
  }
147
152
  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 });
153
+ const raw = options.localPath;
154
+ const resolved = typeof raw === 'string'
155
+ ? path.resolve(raw)
156
+ : {
157
+ ios: raw.ios ? path.resolve(raw.ios) : undefined,
158
+ android: raw.android ? path.resolve(raw.android) : undefined,
159
+ };
160
+ const preview = typeof resolved === 'string'
161
+ ? resolved
162
+ : `ios=${resolved.ios ?? 'auto'}, android=${resolved.android ?? 'auto'}`;
163
+ logOnce(`🔧 [expo-iap] Enabling local OpenIAP: ${preview}`);
164
+ result = (0, withLocalOpenIAP_1.default)(result, { localPath: resolved });
151
165
  }
152
166
  }
153
167
  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;