expo-iap 5.6.2 → 5.7.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 (79) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +5 -5
  3. package/README.md +2 -2
  4. package/android/build.gradle +10 -15
  5. package/android/openiap-store.gradle +365 -0
  6. package/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt +2 -5
  7. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +2 -6
  8. package/build/ExpoIapModule.d.ts +51 -6
  9. package/build/ExpoIapModule.d.ts.map +1 -1
  10. package/build/ExpoIapModule.js +12 -8
  11. package/build/ExpoIapModule.js.map +1 -1
  12. package/build/index.d.ts +22 -44
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +26 -54
  15. package/build/index.js.map +1 -1
  16. package/build/index.kepler.d.ts +2 -2
  17. package/build/index.kepler.js +2 -2
  18. package/build/index.kepler.js.map +1 -1
  19. package/build/kit-api.d.ts +10 -10
  20. package/build/kit-api.d.ts.map +1 -1
  21. package/build/kit-api.js +11 -35
  22. package/build/kit-api.js.map +1 -1
  23. package/build/modules/android.d.ts +5 -7
  24. package/build/modules/android.d.ts.map +1 -1
  25. package/build/modules/android.js +10 -16
  26. package/build/modules/android.js.map +1 -1
  27. package/build/modules/ios.d.ts +12 -30
  28. package/build/modules/ios.d.ts.map +1 -1
  29. package/build/modules/ios.js +14 -37
  30. package/build/modules/ios.js.map +1 -1
  31. package/build/onside/index.d.ts +7 -7
  32. package/build/onside/index.d.ts.map +1 -1
  33. package/build/onside/index.js +7 -8
  34. package/build/onside/index.js.map +1 -1
  35. package/build/types.d.ts +40 -40
  36. package/build/types.js.map +1 -1
  37. package/build/useIAP.d.ts +4 -7
  38. package/build/useIAP.d.ts.map +1 -1
  39. package/build/useIAP.js +11 -32
  40. package/build/useIAP.js.map +1 -1
  41. package/build/utils/debug.d.ts +3 -3
  42. package/build/utils/debug.d.ts.map +1 -1
  43. package/build/utils/debug.js +7 -24
  44. package/build/utils/debug.js.map +1 -1
  45. package/build/utils/errorMapping.d.ts +4 -12
  46. package/build/utils/errorMapping.d.ts.map +1 -1
  47. package/build/utils/errorMapping.js +4 -12
  48. package/build/utils/errorMapping.js.map +1 -1
  49. package/build/utils/restorePurchases.d.ts.map +1 -1
  50. package/build/utils/restorePurchases.js +3 -4
  51. package/build/utils/restorePurchases.js.map +1 -1
  52. package/ios/ExpoIap.podspec +4 -7
  53. package/ios/ExpoIapHelper.swift +2 -3
  54. package/ios/ExpoIapModule.swift +2 -3
  55. package/openiap-versions.json +1 -1
  56. package/package.json +2 -2
  57. package/plugin/build/withIAP.d.ts +16 -5
  58. package/plugin/build/withIAP.js +194 -166
  59. package/plugin/build/withLocalOpenIAP.d.ts +15 -12
  60. package/plugin/build/withLocalOpenIAP.js +139 -113
  61. package/plugin/build/withVega.d.ts +7 -0
  62. package/plugin/build/withVega.js +43 -13
  63. package/plugin/src/expoConfig.augmentation.d.ts +22 -11
  64. package/plugin/src/withIAP.ts +251 -226
  65. package/plugin/src/withLocalOpenIAP.ts +221 -159
  66. package/plugin/src/withVega.ts +45 -13
  67. package/scripts/update-types.mjs +12 -8
  68. package/src/ExpoIapModule.ts +136 -18
  69. package/src/index.kepler.ts +2 -2
  70. package/src/index.ts +28 -65
  71. package/src/kit-api.ts +21 -46
  72. package/src/modules/android.ts +12 -27
  73. package/src/modules/ios.ts +22 -45
  74. package/src/onside/index.ts +7 -9
  75. package/src/types.ts +40 -40
  76. package/src/useIAP.ts +15 -39
  77. package/src/utils/debug.ts +13 -31
  78. package/src/utils/errorMapping.ts +4 -12
  79. package/src/utils/restorePurchases.ts +3 -4
package/AGENTS.md CHANGED
@@ -84,7 +84,7 @@ For complete type definitions and documentation, see: <https://openiap.dev/docs/
84
84
 
85
85
  The library follows the OpenIAP type specifications with platform-specific extensions using iOS/Android suffixes.
86
86
 
87
- > **Important:** `src/types.ts` is generated from the OpenIAP schema. Never edit this file manually or commit hand-written changes. In this monorepo, run `cd specs/client && bun run generate` from the repository root so every platform copy stays synchronized. The Expo-local `bun run generate:types` command is only for a standalone checkout: it atomically refreshes the platform-ready file from the raw `docs-${spec}` tag pinned by `openiap-versions.json` (or the version passed with `--tag`).
87
+ > **Important:** `src/types.ts` is generated from the OpenIAP schema. Never edit this file manually or commit hand-written changes. In this monorepo, run `cd specs/client && bun run generate` from the repository root so every platform copy stays synchronized. The Expo-local `bun run generate:types` command is only for a standalone checkout: it atomically refreshes the platform-ready file from the raw `openiap-client-protocol-${clientProtocol}` tag pinned by `openiap-versions.json` (or the version passed with `--tag`).
88
88
 
89
89
  - Whenever you need Request/Params/Result types in the JS API surface (`src/index.ts`, hooks, modules, examples), import them directly from the generated `src/types.ts` (e.g., `MutationRequestPurchaseArgs`, `QueryFetchProductsArgs`). Bind exported functions with the generated `QueryField` / `MutationField` helpers so their signatures stay in lockstep with `types.ts` instead of redefining ad-hoc unions like `ProductTypeInput`.
90
90
 
package/CONTRIBUTING.md CHANGED
@@ -214,28 +214,28 @@ For detailed code conventions, naming standards, and implementation guidelines,
214
214
 
215
215
  ### Updating OpenIAP Types
216
216
 
217
- The generated TypeScript definitions in `src/types.ts` come from the platform-ready file committed in the [OpenIAP](https://github.com/hyodotdev/openiap) raw `docs-${spec}` tag. Never edit this file by hand. When the schema changes or you need to pull newer published types:
217
+ The generated TypeScript definitions in `src/types.ts` come from the platform-ready file committed in the [OpenIAP](https://github.com/hyodotdev/openiap) raw `openiap-client-protocol-${clientProtocol}` tag. Never edit this file by hand. When the schema changes or you need to pull newer published types:
218
218
 
219
219
  - Run `bun run generate:types` to download the version pinned by `openiap-versions.json`, validate it, and atomically replace `src/types.ts`.
220
- - To target a specific published spec, pass its version: `bun run generate:types --tag <version>` (the script resolves it to `docs-<version>`).
220
+ - To target a specific published Client Protocol version, pass it: `bun run generate:types --tag <version>` (the script resolves it to `openiap-client-protocol-<version>`).
221
221
  - Commit the updated file alongside any related schema or documentation changes.
222
222
 
223
223
  Always ensure the repository builds and tests succeed after regenerating the types.
224
224
 
225
225
  ## 🔢 OpenIAP Version Management
226
226
 
227
- The native dependency and OpenIAP specification versions are sourced from `openiap-versions.json` at the repository root:
227
+ The native dependency and Client Protocol versions are sourced from `openiap-versions.json` at the repository root:
228
228
 
229
229
  - `apple` → iOS Pod dependency (`ios/ExpoIap.podspec`).
230
230
  - `google` → Android artifact (`android/build.gradle`, Expo config plugin).
231
- - `spec` → published OpenIAP specification/types release (`scripts/update-types.mjs`).
231
+ - `clientProtocol` → published Client Protocol types release (`scripts/update-types.mjs`).
232
232
 
233
233
  When bumping dependencies:
234
234
 
235
235
  1. Update the relevant fields in `openiap-versions.json`.
236
236
  2. For iOS changes, run `cd ios && pod install` (and commit the Pod.lock if required by the workflow).
237
237
  3. For Android, re-run Gradle (`bun run android`) so the new artifact is pulled down.
238
- 4. When the `spec` value changes, run `bun run generate:types` to refresh `src/types.ts` from the matching raw `docs-${spec}` tag.
238
+ 4. When the `clientProtocol` value changes, run `bun run generate:types` to refresh `src/types.ts` from the matching raw `openiap-client-protocol-${clientProtocol}` tag.
239
239
  5. Commit the updated JSON, regenerated files, and any resulting lockfile changes together.
240
240
 
241
241
  If the JSON file is missing or malformed, build scripts (Gradle, Podspec, the type generator) will fail fast — fix the JSON rather than hard-coding version strings in multiple locations.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  [![Version](http://img.shields.io/npm/v/expo-iap.svg?style=flat-square)](https://npmjs.org/package/expo-iap) [![Download](http://img.shields.io/npm/dm/expo-iap.svg?style=flat-square)](https://npmjs.org/package/expo-iap) [![OpenIAP](https://img.shields.io/badge/OpenIAP-Compliant-green?style=flat-square)](https://openiap.dev) [![CI](https://github.com/hyodotdev/openiap/actions/workflows/ci-expo-iap.yml/badge.svg?branch=main)](https://github.com/hyodotdev/openiap/actions/workflows/ci-expo-iap.yml?query=branch%3Amain) [![codecov](https://codecov.io/gh/hyodotdev/openiap/branch/main/graph/badge.svg?component=expo-iap)](https://app.codecov.io/gh/hyodotdev/openiap/tree/main/libraries/expo-iap) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fhyochan%2Fexpo-iap.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fhyochan%2Fexpo-iap?ref=badge_shield&issueType=license)
7
7
 
8
- Expo IAP is a powerful in-app purchase solution for Expo and React Native applications that conforms to the OpenIAP specification. It provides a unified API for handling in-app purchases across iOS and Android platforms with comprehensive error handling and modern TypeScript support.
8
+ Expo IAP is a powerful in-app purchase solution for Expo and React Native applications that conforms to the OpenIAP Client Protocol. It provides a unified API for handling in-app purchases across iOS and Android platforms with comprehensive error handling and modern TypeScript support.
9
9
 
10
10
  If you're shipping an app with expo-iap, we’d love to hear about it—please share your product and feedback in [expo-iap Q&A Discussions](https://github.com/hyodotdev/openiap/discussions/categories/expo-iap). Community stories help us keep improving the ecosystem.
11
11
 
@@ -66,7 +66,7 @@ For detailed usage examples and error handling, see the [documentation](https://
66
66
 
67
67
  <a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.webp" alt="OpenIAP" height="50" /></a>
68
68
 
69
- Expo IAP conforms to the **[OpenIAP specification](https://openiap.dev)** — an open, vendor-neutral interoperability standard for in-app purchases. OpenIAP provides:
69
+ Expo IAP conforms to the **[OpenIAP Client Protocol](https://openiap.dev)** — an open, vendor-neutral interoperability standard for in-app purchases. OpenIAP provides:
70
70
 
71
71
  - **Shared specification** — Common types, error codes, and purchase flows across all platforms
72
72
  - **Generated type-safe bindings** — Swift, Kotlin, Dart, and GDScript from a single GraphQL schema
@@ -53,12 +53,9 @@ if (!(googleVersion instanceof String) || !googleVersion.trim()) {
53
53
  def googleVersionString = googleVersion.trim()
54
54
  apply from: project.file('openiap-android-sdk.gradle')
55
55
 
56
- def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false
57
- def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false
58
- if (horizonEnabled && fireOsEnabled) {
59
- throw new GradleException("expo-iap: horizonEnabled and fireOsEnabled cannot both be true")
60
- }
61
- def openiapFlavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play')
56
+ // Store selection: explicit property > task flavor > connected debug device > play.
57
+ apply from: project.file('openiap-store.gradle')
58
+ def openiapFlavor = openIapResolveStore('expo-iap').store
62
59
 
63
60
  // If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
64
61
  // The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
@@ -111,17 +108,15 @@ dependencies {
111
108
  testImplementation "junit:junit:4.13.2"
112
109
  testImplementation "org.json:json:20260719"
113
110
 
114
- // Use OpenIAP Google module only; avoid direct BillingClient dependency
111
+ // `api`, so an app's own native code can use dev.hyo.openiap through this module.
115
112
  if (findProject(":openiap-google") != null) {
116
- implementation project(":openiap-google")
117
- } else if (fireOsEnabled) {
118
- // Use openiap-google-amazon for Fire OS when fireOsEnabled is true
119
- implementation "io.github.hyochan.openiap:openiap-google-amazon:${googleVersionString}"
120
- } else if (horizonEnabled) {
121
- // Use openiap-google-horizon for Meta Quest when horizonEnabled is true
122
- implementation "io.github.hyochan.openiap:openiap-google-horizon:${googleVersionString}"
113
+ api project(":openiap-google")
114
+ } else if (openiapFlavor == 'amazon') {
115
+ api "io.github.hyochan.openiap:openiap-google-amazon:${googleVersionString}"
116
+ } else if (openiapFlavor == 'horizon') {
117
+ api "io.github.hyochan.openiap:openiap-google-horizon:${googleVersionString}"
123
118
  } else {
124
119
  // Fallback to published artifact when local project isn't linked
125
- implementation "io.github.hyochan.openiap:openiap-google:${googleVersionString}"
120
+ api "io.github.hyochan.openiap:openiap-google:${googleVersionString}"
126
121
  }
127
122
  }
@@ -0,0 +1,365 @@
1
+ // OpenIAP store resolver. Framework wrappers symlink this file into android/
2
+ // and publish it as a real file.
3
+ // Apply it, then call openIapResolveStore('<label>'), or ('<label>', [allowNone: true])
4
+ // where the library can build without a store SDK.
5
+ //
6
+ // First match wins:
7
+ // 1. explicit openiapStore=<store> (-P, ORG_GRADLE_PROJECT_openiapStore, gradle.properties).
8
+ // The legacy horizonEnabled, fireOsEnabled and openiapPlatform=none still work.
9
+ // 2. variant a requested task names a store flavor (assembleHorizonRelease).
10
+ // 3. device debug builds only: the Quest or Fire device that ANDROID_SERIAL
11
+ // names, or the only one connected.
12
+ // 4. play
13
+ // Two signals naming different stores fail the build. Only spelled-out task names
14
+ // are read; a task that builds another store (an abbreviation such as aHR) fails
15
+ // once the task graph is ready.
16
+
17
+ ext.openIapStoreAliases = [
18
+ play: 'play', google: 'play', gplay: 'play', googleplay: 'play', 'google-play': 'play', gms: 'play',
19
+ horizon: 'horizon', meta: 'horizon', quest: 'horizon',
20
+ amazon: 'amazon', fire: 'amazon', fireos: 'amazon', 'fire-os': 'amazon',
21
+ none: 'none', auto: 'auto',
22
+ ]
23
+
24
+ ext.openIapNormalizeStore = { Object value, String label ->
25
+ def key = value?.toString()?.trim()?.toLowerCase(Locale.ROOT)
26
+ if (!key) {
27
+ return null
28
+ }
29
+ def store = openIapStoreAliases[key]
30
+ if (store == null) {
31
+ throw new GradleException("${label}: unknown openiapStore '${value}'; use play, horizon, amazon, auto, or none where the library allows it")
32
+ }
33
+ return store
34
+ }
35
+
36
+ // assembleHorizonRelease -> [assemble, horizon, release]
37
+ ext.openIapTaskTokens = { String taskPath ->
38
+ def name = taskPath.substring(taskPath.lastIndexOf(':') + 1)
39
+ return name.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|[^A-Za-z0-9]+/).collect { it.toLowerCase(Locale.ROOT) }.findAll { it }
40
+ }
41
+
42
+ // The key that set the opt-out, for error messages.
43
+ ext.openIapOptOutKey = {
44
+ def explicit = project.findProperty('openiapStore')?.toString()?.trim()?.toLowerCase(Locale.ROOT)
45
+ return explicit == 'none' ? 'openiapStore=none' : 'openiapPlatform=none'
46
+ }
47
+
48
+ ext.openIapExplicitStore = { String label, boolean allowNone ->
49
+ def explicit = openIapNormalizeStore(project.findProperty('openiapStore'), label)
50
+ def horizon = project.findProperty('horizonEnabled')?.toString()?.toBoolean() ?: false
51
+ def fireOs = project.findProperty('fireOsEnabled')?.toString()?.toBoolean() ?: false
52
+ def platform = project.findProperty('openiapPlatform')?.toString()?.trim()?.toLowerCase(Locale.ROOT)
53
+ if (platform != null && platform != 'none') {
54
+ throw new GradleException("${label}: openiapPlatform only supports the opt-out value 'none'; use openiapStore to pick a store")
55
+ }
56
+ if (horizon && fireOs) {
57
+ throw new GradleException("${label}: horizonEnabled and fireOsEnabled cannot both be true")
58
+ }
59
+ if (platform == 'none' && (horizon || fireOs)) {
60
+ throw new GradleException("${label}: openiapPlatform=none conflicts with legacy store flags")
61
+ }
62
+ def (legacyKey, legacy) = platform == 'none' ? ['openiapPlatform=none', 'none']
63
+ : fireOs ? ['fireOsEnabled=true', 'amazon']
64
+ : horizon ? ['horizonEnabled=true', 'horizon'] : [null, null]
65
+ if (legacy != null) {
66
+ logger.warn("${label}: horizonEnabled, fireOsEnabled, and openiapPlatform are deprecated; use openiapStore=${legacy}")
67
+ }
68
+ def pin = explicit == 'auto' ? null : explicit
69
+ if (pin != null && legacy != null && legacy != pin) {
70
+ throw new GradleException("${label}: openiapStore=${pin} conflicts with ${legacyKey}")
71
+ }
72
+ def store = pin ?: legacy
73
+ if (store == 'none' && !allowNone) {
74
+ throw new GradleException("${label}: ${openIapOptOutKey()} is not supported by this library")
75
+ }
76
+ return store
77
+ }
78
+
79
+ // Task options that take a separate value, from Gradle's `help --task` output.
80
+ // An unlisted option's value may be read as a task; the graph check then fails
81
+ // unless the value also names a task the build runs.
82
+ ext.openIapValueOptions = [
83
+ '-b', '-c', '-g', '-I', '-p',
84
+ '--args', '--component', '--configuration', '--console', '--dependency',
85
+ '--distribution-type', '--dsl', '--format',
86
+ '--gradle-distribution-sha256-sum', '--gradle-distribution-url',
87
+ '--gradle-version', '--group', '--groups', '--include-build',
88
+ '--insecure-protocol', '--java-version', '--jvm-vendor', '--jvm-version',
89
+ '--max-workers', '--network-timeout', '--package', '--project-cache-dir',
90
+ '--project-dir', '--project-name', '--property', '--task', '--test-framework',
91
+ '--tests', '--type', '--variant',
92
+ ] as Set
93
+
94
+ // Gradle flattens an invocation into one list, options included, so drop each
95
+ // option and its value: `--tests *AmazonTest*` is not an Amazon build.
96
+ ext.openIapRequestedTasks = {
97
+ def names = []
98
+ def previous = null
99
+ def requested = gradle.startParameter.taskNames
100
+ if (requested.isEmpty()) {
101
+ requested = rootProject.defaultTasks ?: []
102
+ }
103
+ requested.each { String token ->
104
+ if (!token) {
105
+ return
106
+ }
107
+ if (token.startsWith('-')) {
108
+ previous = token
109
+ return
110
+ }
111
+ if (previous != null && openIapValueOptions.contains(previous)) {
112
+ previous = token
113
+ return
114
+ }
115
+ names << token
116
+ previous = token
117
+ }
118
+ return names
119
+ }
120
+
121
+ // Variant detection reads every requested task: keep whole-token
122
+ // play/horizon/amazon out of custom task names.
123
+ ext.openIapTaskStore = { String label ->
124
+ def named = openIapRequestedTasks().collectMany { openIapTaskTokens(it) }
125
+ .findAll { it in ['play', 'horizon', 'amazon'] } as LinkedHashSet
126
+ // Two stores in one build: refuse rather than guess.
127
+ if (named.size() > 1) {
128
+ throw new GradleException("${label}: cannot tell which store the requested tasks '${openIapRequestedTasks().join(' ')}' select (${named.join(', ')}); build one store per invocation, spell the tasks out, or set openiapStore")
129
+ }
130
+ return named ? named.first() : null
131
+ }
132
+
133
+ // `clean assembleDebug` is a debug build; any release task makes it a release
134
+ // build, which never depends on the attached device.
135
+ ext.openIapBuildTypes = { String taskPath ->
136
+ def types = [] as LinkedHashSet
137
+ openIapTaskTokens(taskPath).each { token ->
138
+ if (token in ['debug', 'release']) {
139
+ types << token
140
+ }
141
+ }
142
+ if (types) {
143
+ return types
144
+ }
145
+ // `aD` is assembleDebug, so the device rule has to see it as one.
146
+ def name = taskPath.substring(taskPath.lastIndexOf(':') + 1)
147
+ def segments = name.split(/(?=[A-Z])/).collect { it.toLowerCase(Locale.ROOT) }.findAll { it }
148
+ segments.drop(1).each { segment ->
149
+ ['debug', 'release'].findAll { it.startsWith(segment) }.each { types << it }
150
+ }
151
+ return types
152
+ }
153
+
154
+ ext.openIapDebugBuild = {
155
+ def names = openIapRequestedTasks()
156
+ if (!names) {
157
+ return false
158
+ }
159
+ def types = names.collectMany { openIapBuildTypes(it) } as LinkedHashSet
160
+ return types.contains('debug') && !types.contains('release')
161
+ }
162
+
163
+ // Gradle resolves a typed name to real tasks: an exact, case-insensitive match
164
+ // when one exists, otherwise a unique prefix or a camelCase abbreviation.
165
+ ext.openIapSelectedTasks = { graph, String request ->
166
+ def wanted = request.substring(request.lastIndexOf(':') + 1)
167
+ if (!wanted) {
168
+ return []
169
+ }
170
+ def lower = wanted.toLowerCase(Locale.ROOT)
171
+ def names = graph.allTasks.collect { it.name } as LinkedHashSet
172
+ def exact = names.findAll { it.toLowerCase(Locale.ROOT) == lower }
173
+ if (exact) {
174
+ return exact
175
+ }
176
+ def segments = wanted.split(/(?=[A-Z])/).findAll { it }
177
+ return names.findAll { String name ->
178
+ if (name.toLowerCase(Locale.ROOT).startsWith(lower)) {
179
+ return true
180
+ }
181
+ def taskSegments = name.split(/(?=[A-Z])/).findAll { it }
182
+ if (segments.size() < 2 || segments.size() > taskSegments.size()) {
183
+ return false
184
+ }
185
+ return (0..<segments.size()).every {
186
+ taskSegments[it].toLowerCase(Locale.ROOT).startsWith(segments[it].toLowerCase(Locale.ROOT))
187
+ }
188
+ }
189
+ }
190
+
191
+ // Checks the store picked at configuration against the tasks that will run.
192
+ // Only tasks the request selected count: an anchor such as assembleDebug also
193
+ // builds every flavor of a library it reaches.
194
+ ext.openIapVerifyTaskGraph = { String label, Map resolution ->
195
+ def resolved = resolution.store
196
+ if (resolved == 'none') {
197
+ return
198
+ }
199
+ gradle.taskGraph.whenReady { graph ->
200
+ def named = [] as LinkedHashSet
201
+ openIapRequestedTasks().each { String request ->
202
+ openIapSelectedTasks(graph, request).each { String taskName ->
203
+ openIapTaskTokens(taskName).each { token ->
204
+ if (token in ['play', 'horizon', 'amazon']) {
205
+ named << token
206
+ }
207
+ }
208
+ }
209
+ }
210
+ if (!named) {
211
+ // The store came from something that is not a task, such as an option's value.
212
+ if (resolution.source == 'variant') {
213
+ throw new GradleException("${label}: read ${resolved} from '${openIapRequestedTasks().join(' ')}', but no task it runs names a store; set openiapStore")
214
+ }
215
+ return
216
+ }
217
+ if (named.size() > 1) {
218
+ throw new GradleException("${label}: the requested tasks build more than one store (${named.join(', ')}) but a build links one; build one store per invocation or set openiapStore")
219
+ }
220
+ def actual = named.first()
221
+ if (actual != resolved) {
222
+ throw new GradleException("${label}: linked ${resolved} but the requested tasks build ${actual}; spell the task out or set openiapStore=${actual}")
223
+ }
224
+ }
225
+ }
226
+
227
+ ext.openIapEnv = { String name ->
228
+ // providers.environmentVariable is a tracked configuration-cache input;
229
+ // System.getenv is not.
230
+ return providers.environmentVariable(name).orNull
231
+ }
232
+
233
+ ext.openIapAdb = {
234
+ def roots = [openIapEnv('ANDROID_HOME'), openIapEnv('ANDROID_SDK_ROOT')]
235
+ // local.properties is plain I/O, not a tracked input: sdk.dir ~never
236
+ // changes, and staleness only loses adb discovery, never a store pick.
237
+ def localProperties = rootProject.file('local.properties')
238
+ if (localProperties.isFile()) {
239
+ def properties = new Properties()
240
+ localProperties.withInputStream { properties.load(it) }
241
+ roots << properties.getProperty('sdk.dir')
242
+ }
243
+ def names = ['adb', 'adb.exe']
244
+ def adb = roots.findResult { root ->
245
+ if (!root) {
246
+ return null
247
+ }
248
+ return names.findResult { name ->
249
+ def candidate = new File(root, "platform-tools/${name}")
250
+ return candidate.isFile() ? candidate.absolutePath : null
251
+ }
252
+ }
253
+ if (adb != null) {
254
+ return adb
255
+ }
256
+ // A developer with adb only on PATH still gets device selection.
257
+ return (openIapEnv('PATH') ?: '').split(File.pathSeparator).findResult { entry ->
258
+ if (!entry) {
259
+ return null
260
+ }
261
+ return names.findResult { name ->
262
+ def candidate = new File(entry, name)
263
+ return candidate.isFile() ? candidate.absolutePath : null
264
+ }
265
+ }
266
+ }
267
+
268
+ // providers.exec keeps the probe legal under the configuration cache, and Gradle
269
+ // re-runs it before reusing a cached configuration, so a new device reconfigures.
270
+ ext.openIapAdbText = { List<String> command ->
271
+ try {
272
+ return providers.exec {
273
+ commandLine(command)
274
+ ignoreExitValue = true
275
+ }.standardOutput.asText.get()
276
+ } catch (Throwable ignored) {
277
+ return null
278
+ }
279
+ }
280
+
281
+ // Like every Android tool, target the device ANDROID_SERIAL names.
282
+ ext.openIapDeviceStore = {
283
+ def adb = openIapAdb()
284
+ if (adb == null) {
285
+ return null
286
+ }
287
+ def listing = openIapAdbText([adb, 'devices'])
288
+ if (listing == null) {
289
+ return null
290
+ }
291
+ // Windows adb terminates lines with CR, which the serial would otherwise keep.
292
+ def serials = listing.readLines()
293
+ .collect { it.replace('\r', '').trim() }
294
+ .findAll { it ==~ /^\S+\s+device$/ }
295
+ .collect { it.split(/\s+/)[0] }
296
+ def requested = openIapEnv('ANDROID_SERIAL')?.trim()
297
+ def serial
298
+ if (requested) {
299
+ if (!serials.contains(requested)) {
300
+ logger.warn("openiap: ANDROID_SERIAL=${requested} is not attached; not selecting a store from a device")
301
+ return null
302
+ }
303
+ serial = requested
304
+ } else if (serials.size() == 1) {
305
+ serial = serials[0]
306
+ } else {
307
+ return null
308
+ }
309
+ def features = openIapAdbText([adb, '-s', serial, 'shell', 'pm', 'list', 'features'])
310
+ def manufacturerText = openIapAdbText([adb, '-s', serial, 'shell', 'getprop', 'ro.product.manufacturer'])
311
+ // A device that dropped after the listing answers with nothing; that is not Play.
312
+ if (!features?.trim() && !manufacturerText?.trim()) {
313
+ logger.warn("openiap: ${serial} stopped responding; not selecting a store from a device")
314
+ return null
315
+ }
316
+ features = features ?: ''
317
+ def manufacturer = (manufacturerText ?: '').trim()
318
+ def store = 'play'
319
+ if (features.contains('feature:horizonos.software.horizon_os') || features.contains('feature:oculus.hardware.standalone_vr')) {
320
+ store = 'horizon'
321
+ } else if (features.contains('feature:amazon.hardware.fire_tv') || manufacturer.equalsIgnoreCase('Amazon')) {
322
+ store = 'amazon'
323
+ }
324
+ return [store: store, reason: "device ${serial}, manufacturer ${manufacturer ?: 'unknown'}"]
325
+ }
326
+
327
+ ext.openIapResolveStore = { String label, Map options = [:] ->
328
+ def allowNone = options.allowNone == true
329
+ if (rootProject.ext.has('openIapStoreResolution')) {
330
+ def cached = rootProject.ext.openIapStoreResolution
331
+ // The opt-out is per caller: a library that needs a store SDK still refuses it.
332
+ if (cached.store == 'none' && !allowNone) {
333
+ throw new GradleException("${label}: ${openIapOptOutKey()} is not supported by this library")
334
+ }
335
+ return cached
336
+ }
337
+ def explicit = openIapExplicitStore(label, allowNone)
338
+ // Opting out links nothing, so the tasks may build several store flavors.
339
+ def variant = explicit == 'none' ? null : openIapTaskStore(label)
340
+ // A pin against the task's flavor would ship the wrong SDK. The opt-out links
341
+ // nothing, so it may sit beside any flavor.
342
+ if (explicit != null && explicit != 'none' && variant != null && explicit != variant) {
343
+ throw new GradleException(
344
+ "${label}: openiapStore=${explicit} conflicts with the ${variant} flavor in '${openIapRequestedTasks().join(' ')}'"
345
+ )
346
+ }
347
+ def resolution = null
348
+ if (explicit != null) {
349
+ resolution = [store: explicit, source: 'explicit', reason: 'openiapStore or a legacy flag']
350
+ } else if (variant != null) {
351
+ resolution = [store: variant, source: 'variant', reason: "task ${openIapRequestedTasks().join(' ')}"]
352
+ } else if (openIapDebugBuild()) {
353
+ def device = openIapDeviceStore()
354
+ if (device != null) {
355
+ resolution = [store: device.store, source: 'device', reason: device.reason]
356
+ }
357
+ }
358
+ if (resolution == null) {
359
+ resolution = [store: 'play', source: 'default', reason: 'no store signal']
360
+ }
361
+ rootProject.ext.openIapStoreResolution = resolution
362
+ logger.lifecycle("openiap: store=${resolution.store} (source=${resolution.source}; ${resolution.reason})")
363
+ openIapVerifyTaskGraph(label, resolution)
364
+ return resolution
365
+ }
@@ -272,10 +272,7 @@ object ExpoIapHelper {
272
272
  )
273
273
  }
274
274
 
275
- /**
276
- * Helper to safely emit an event with error fallback.
277
- * Reduces code duplication across listener handlers.
278
- */
275
+ /** Emits an event, falling back to a purchase error if emitting fails. */
279
276
  private fun safeEmitEvent(
280
277
  module: Module,
281
278
  scope: CoroutineScope,
@@ -362,7 +359,7 @@ object ExpoIapHelper {
362
359
  )
363
360
  }.onFailure { error ->
364
361
  ExpoIapLog.failure("buffer/send PURCHASE_ERROR", error)
365
- // Critical: if we can't emit the original error, at least try to emit a generic one
362
+ // If the original error cannot be emitted, try a generic one.
366
363
  val fallbackPayload =
367
364
  mapOf(
368
365
  "code" to OpenIapError.UnknownError.CODE,
@@ -148,8 +148,8 @@ class ExpoIapModule : Module() {
148
148
  scope.launch {
149
149
  connectionMutex.withLock {
150
150
  try {
151
- // CRITICAL: Set Activity BEFORE calling initConnection
152
- // Horizon SDK needs Activity to initialize OVRPlatform with proper returnComponent
151
+ // Set the Activity before initConnection: the Horizon SDK needs it to
152
+ // initialize OVRPlatform with the proper returnComponent.
153
153
  // https://github.com/meta-quest/Meta-Spatial-SDK-Samples/issues/82#issuecomment-3452577530
154
154
  runCatching { currentActivity }
155
155
  .onSuccess {
@@ -501,7 +501,6 @@ class ExpoIapModule : Module() {
501
501
  }
502
502
  }
503
503
 
504
- // New name: consumePurchaseAndroid
505
504
  AsyncFunction("consumePurchaseAndroid") { token: String, promise: Promise ->
506
505
  ExpoIapLog.payload("consumePurchaseAndroid", mapOf("token" to token))
507
506
  scope.launch {
@@ -610,9 +609,6 @@ class ExpoIapModule : Module() {
610
609
  scope.launch {
611
610
  try {
612
611
  val openIapProgram = mapBillingProgram(program)
613
- // Note: enableBillingProgram should be called before initConnection
614
- // for proper BillingClient configuration. Here it's called as a fallback
615
- // but may have no effect if BillingClient is already initialized.
616
612
  val result = openIapStore.isBillingProgramAvailable(openIapProgram)
617
613
  val response =
618
614
  mapOf(
@@ -1,11 +1,56 @@
1
+ import { type EventSubscription } from 'expo-modules-core';
2
+ import type { BillingProgramAndroid, BillingProgramReportingDetailsAndroid, DeveloperBillingTypeAndroid, Mutation, MutationField, ProductQueryType, PurchaseInput, PurchaseOptions, PurchaseUpdatedListenerOptions, Query, QueryField, SubscriptionStatusIOS } from './types';
3
+ /** Members read from the raw module; every store module provides them. */
4
+ type NativeEventModule = {
5
+ ERROR_CODES?: Record<string, unknown>;
6
+ addListener<T>(eventName: string, listener: (payload: T) => void): EventSubscription | undefined;
7
+ removeListener?<T>(eventName: string, listener: (payload: T) => void): void;
8
+ setPurchaseUpdatedListenerOptions?(options?: PurchaseUpdatedListenerOptions | null): Promise<void>;
9
+ };
10
+ type QueryFields<K extends keyof Query> = {
11
+ [P in K]: QueryField<P>;
12
+ };
13
+ type MutationFields<K extends keyof Mutation> = {
14
+ [P in K]: MutationField<P>;
15
+ };
16
+ /**
17
+ * Native surface behind the default export. Results the wrappers decode stay
18
+ * `unknown`; the rest match the generated operation signatures.
19
+ */
20
+ export type ExpoIapNativeModule = NativeEventModule & QueryFields<'canPresentExternalPurchaseNoticeIOS' | 'currentEntitlementIOS' | 'getActiveSubscriptions' | 'getAppTransactionIOS' | 'getBillingChoiceInfoAndroid' | 'getExternalPurchaseCustomLinkTokenIOS' | 'getPromotedProductIOS' | 'getReceiptDataIOS' | 'getTransactionJwsIOS' | 'hasActiveSubscriptions' | 'isEligibleForExternalPurchaseCustomLinkIOS' | 'isEligibleForIntroOfferIOS' | 'isTransactionVerifiedIOS' | 'latestTransactionIOS'> & MutationFields<'beginRefundRequestIOS' | 'clearTransactionIOS' | 'endConnection' | 'initConnection' | 'isBillingProgramAvailableAndroid' | 'launchExternalLinkAndroid' | 'openRedeemOfferCodeAndroid' | 'presentCodeRedemptionSheetIOS' | 'presentExternalPurchaseLinkIOS' | 'presentExternalPurchaseNoticeSheetIOS' | 'showBillingProgramInformationDialogAndroid' | 'showExternalPurchaseCustomLinkNoticeIOS' | 'showInAppMessagesAndroid' | 'syncIOS' | 'verifyPurchase' | 'verifyPurchaseWithProvider'> & {
21
+ USING_ONSIDE_SDK: boolean;
22
+ USING_VEGA_SDK: boolean;
23
+ fetchProducts(request: {
24
+ skus: string[];
25
+ type: ProductQueryType;
26
+ }): Promise<unknown[]>;
27
+ fetchProducts(type: ProductQueryType, skus: string[]): Promise<unknown[]>;
28
+ getAvailableItems(alsoPublishToEventListenerIOS: boolean, onlyIncludeActiveItemsIOS: boolean): Promise<unknown>;
29
+ getAvailableItems(options: PurchaseOptions): Promise<unknown>;
30
+ getAllTransactionsIOS(): Promise<unknown>;
31
+ getPendingTransactionsIOS(): Promise<unknown>;
32
+ showManageSubscriptionsIOS(): Promise<unknown>;
33
+ requestPurchase(request: object): Promise<unknown>;
34
+ finishTransaction(purchase: PurchaseInput, isConsumable: boolean | null): Promise<boolean>;
35
+ acknowledgePurchaseAndroid(purchaseToken: string): Promise<unknown>;
36
+ consumePurchaseAndroid(purchaseToken: string): Promise<unknown>;
37
+ createBillingProgramReportingDetailsAndroid(program: BillingProgramAndroid, developerBillingType: DeveloperBillingTypeAndroid | null): Promise<BillingProgramReportingDetailsAndroid>;
38
+ subscriptionStatusIOS(sku: string): Promise<SubscriptionStatusIOS[] | null>;
39
+ requestReceiptRefreshIOS(): Promise<string>;
40
+ deepLinkToSubscriptionsAndroid?(options: {
41
+ skuAndroid?: string;
42
+ packageNameAndroid?: string;
43
+ }): Promise<void> | void;
44
+ getStorefront?(): Promise<string> | string;
45
+ restorePurchases?(): Promise<boolean>;
46
+ };
1
47
  export declare const NATIVE_ERROR_CODES: Record<string, unknown>;
2
48
  /**
3
- * Returns the raw native module (not wrapped in a Proxy).
4
- * Use this for EventEmitter / addListener calls — JSI HostObjects
5
- * require the real native module as `this`; a Proxy triggers
6
- * "native state unsupported on Proxy" on New Architecture / Hermes.
49
+ * Returns the raw native module, not the Proxy. Use it for addListener: a JSI
50
+ * HostObject needs the real module as `this`, and a Proxy throws "native state
51
+ * unsupported on Proxy" on New Architecture / Hermes.
7
52
  */
8
- export declare function getNativeModule(): any;
9
- declare const _default: any;
53
+ export declare function getNativeModule(): NativeEventModule;
54
+ declare const _default: ExpoIapNativeModule;
10
55
  export default _default;
11
56
  //# sourceMappingURL=ExpoIapModule.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoIapModule.d.ts","sourceRoot":"","sources":["../src/ExpoIapModule.ts"],"names":[],"mappings":"AA6FA,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAQtD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,eAAe,QAE9B;;AAED,wBAuBG"}
1
+ {"version":3,"file":"ExpoIapModule.d.ts","sourceRoot":"","sources":["../src/ExpoIapModule.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { requireNativeModule, UnavailabilityError } from 'expo-modules-core';
1
+ import { requireNativeModule, UnavailabilityError, } from 'expo-modules-core';
2
2
  import { installedFromOnside } from './onside';
3
3
  import { getVegaIapModule, isVegaOS } from './vega';
4
4
  const ONSIDE_MARKETPLACE_ID = 'com.onside.marketplace-app';
@@ -47,7 +47,10 @@ function getResolved() {
47
47
  throw new UnavailabilityError('expo-iap', 'The Onside marketplace build does not contain ExpoIapOnside. Rebuild with ios.onside.enabled instead of routing purchases through Apple StoreKit.');
48
48
  }
49
49
  }
50
- return { module: requireNativeModule('ExpoIap'), name: 'ExpoIap' };
50
+ return {
51
+ module: requireNativeModule('ExpoIap'),
52
+ name: 'ExpoIap',
53
+ };
51
54
  }
52
55
  const expectedName = getExpectedModuleName();
53
56
  if (!cached || cached.name !== expectedName) {
@@ -68,18 +71,19 @@ export const NATIVE_ERROR_CODES = new Proxy({}, {
68
71
  get(target, prop) {
69
72
  if (typeof prop === 'symbol')
70
73
  return Reflect.get(target, prop);
71
- return (getResolved().module.ERROR_CODES || {})[prop];
74
+ const errorCodes = getResolved().module.ERROR_CODES || {};
75
+ return errorCodes[prop];
72
76
  },
73
77
  });
74
78
  /**
75
- * Returns the raw native module (not wrapped in a Proxy).
76
- * Use this for EventEmitter / addListener calls — JSI HostObjects
77
- * require the real native module as `this`; a Proxy triggers
78
- * "native state unsupported on Proxy" on New Architecture / Hermes.
79
+ * Returns the raw native module, not the Proxy. Use it for addListener: a JSI
80
+ * HostObject needs the real module as `this`, and a Proxy throws "native state
81
+ * unsupported on Proxy" on New Architecture / Hermes.
79
82
  */
80
83
  export function getNativeModule() {
81
84
  return getResolved().module;
82
85
  }
86
+ // The Proxy forwards every member to the lazily resolved store module.
83
87
  export default new Proxy({}, {
84
88
  get(target, prop) {
85
89
  if (typeof prop === 'symbol')
@@ -91,7 +95,7 @@ export default new Proxy({}, {
91
95
  if (prop === 'USING_VEGA_SDK') {
92
96
  return resolved.name === 'ExpoIapVega';
93
97
  }
94
- const value = resolved.module[prop];
98
+ const value = Reflect.get(resolved.module, prop);
95
99
  if (value !== undefined || resolved.name !== 'ExpoIapOnside') {
96
100
  return value;
97
101
  }