expo-iap 4.2.7 → 4.3.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.
- package/CLAUDE.md +8 -8
- package/CONTRIBUTING.md +2 -2
- package/README.md +13 -14
- package/android/build.gradle +36 -18
- package/android/openiap-android-sdk.gradle +30 -0
- package/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt +7 -9
- package/android/src/main/java/expo/modules/iap/ExpoIapLog.kt +3 -1
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +12 -11
- package/android/src/main/java/expo/modules/iap/PromiseUtils.kt +2 -3
- package/build/ExpoIapModule.d.ts.map +1 -1
- package/build/ExpoIapModule.js +53 -14
- package/build/ExpoIapModule.js.map +1 -1
- package/build/index.d.ts +20 -20
- package/build/index.d.ts.map +1 -1
- package/build/index.js +210 -66
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +8 -8
- package/build/modules/android.js +8 -8
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +24 -24
- package/build/modules/ios.js +25 -25
- package/build/modules/ios.js.map +1 -1
- package/build/types.d.ts +63 -49
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/useIAP.d.ts +8 -2
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +13 -13
- package/build/useIAP.js.map +1 -1
- package/bun.lock +30 -529
- package/ios/ExpoIapHelper.swift +54 -9
- package/ios/ExpoIapModule.swift +30 -12
- package/ios/onside/OnsideIapModule.swift +128 -41
- package/openiap-versions.json +3 -3
- package/package.json +1 -1
- package/plugin/build/withLocalOpenIAP.js +45 -9
- package/plugin/src/withLocalOpenIAP.ts +94 -14
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/scripts/test-coverage.sh +5 -1
- package/src/ExpoIapModule.ts +60 -19
- package/src/index.ts +286 -80
- package/src/modules/android.ts +8 -8
- package/src/modules/ios.ts +25 -25
- package/src/types.ts +66 -49
- package/src/useIAP.ts +20 -11
package/CLAUDE.md
CHANGED
|
@@ -79,11 +79,11 @@ Before committing any changes:
|
|
|
79
79
|
|
|
80
80
|
- **ID fields**: Use `Id` instead of `ID` (e.g., `productId`, `transactionId`, not `productID`, `transactionID`)
|
|
81
81
|
- **Consistent naming**: This applies to functions, types, and file names
|
|
82
|
-
- **Deprecation**: Fields without platform suffixes
|
|
82
|
+
- **Deprecation**: Fields without platform suffixes are legacy and should only be removed in a planned major release.
|
|
83
83
|
|
|
84
84
|
### Type System
|
|
85
85
|
|
|
86
|
-
For complete type definitions and documentation, see: <https://
|
|
86
|
+
For complete type definitions and documentation, see: <https://openiap.dev/docs/types>
|
|
87
87
|
|
|
88
88
|
The library follows the OpenIAP type specifications with platform-specific extensions using iOS/Android suffixes.
|
|
89
89
|
|
|
@@ -109,7 +109,7 @@ The library follows the OpenIAP type specifications with platform-specific exten
|
|
|
109
109
|
### API Method Naming
|
|
110
110
|
|
|
111
111
|
- Functions that depend on event results should use `request` prefix (e.g., `requestPurchase`)
|
|
112
|
-
- Follow OpenIAP terminology: <https://
|
|
112
|
+
- Follow OpenIAP terminology: <https://openiap.dev/docs/apis#terminology>
|
|
113
113
|
- Do not use generic prefixes like `get`, `find` - refer to the official terminology
|
|
114
114
|
|
|
115
115
|
## IAP-Specific Guidelines
|
|
@@ -118,10 +118,10 @@ The library follows the OpenIAP type specifications with platform-specific exten
|
|
|
118
118
|
|
|
119
119
|
All implementations must follow the OpenIAP specification:
|
|
120
120
|
|
|
121
|
-
- **APIs**: <https://
|
|
122
|
-
- **Types**: <https://
|
|
123
|
-
- **Events**: <https://
|
|
124
|
-
- **Errors**: <https://
|
|
121
|
+
- **APIs**: <https://openiap.dev/docs/apis>
|
|
122
|
+
- **Types**: <https://openiap.dev/docs/types>
|
|
123
|
+
- **Events**: <https://openiap.dev/docs/events>
|
|
124
|
+
- **Errors**: <https://openiap.dev/docs/errors>
|
|
125
125
|
|
|
126
126
|
### Feature Development Process
|
|
127
127
|
|
|
@@ -251,7 +251,7 @@ const {requestPurchase} = useIAP({
|
|
|
251
251
|
|
|
252
252
|
For complete error handling documentation, see:
|
|
253
253
|
|
|
254
|
-
- [Error Codes Reference](https://
|
|
254
|
+
- [Error Codes Reference](https://openiap.dev/docs/errors)
|
|
255
255
|
- [Error Handling Guide](https://docs.expo-iap.dev/docs/guides/error-handling)
|
|
256
256
|
|
|
257
257
|
## Documentation Guidelines
|
package/CONTRIBUTING.md
CHANGED
|
@@ -446,8 +446,8 @@ We welcome feature requests! Please:
|
|
|
446
446
|
|
|
447
447
|
## 📚 Additional Resources
|
|
448
448
|
|
|
449
|
-
- [Documentation Site](https://
|
|
450
|
-
- [API Reference](https://
|
|
449
|
+
- [Documentation Site](https://openiap.dev/docs/setup/expo)
|
|
450
|
+
- [API Reference](https://openiap.dev/docs/apis)
|
|
451
451
|
- [Example App](./example)
|
|
452
452
|
|
|
453
453
|
Thank you for contributing to expo-iap! 🎉
|
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Expo IAP
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
|
-
<img src="https://
|
|
4
|
+
<img src="https://openiap.dev/frameworks/expo.svg" alt="Expo IAP Logo" width="150" />
|
|
5
5
|
|
|
6
6
|
[](https://npmjs.org/package/expo-iap) [](https://npmjs.org/package/expo-iap) [](https://openiap.dev) [](https://github.com/hyodotdev/openiap/actions/workflows/ci.yml) [](https://codecov.io/gh/hyodotdev/openiap) [](https://app.fossa.com/projects/git%2Bgithub.com%2Fhyochan%2Fexpo-iap?ref=badge_shield&issueType=license)
|
|
7
7
|
|
|
8
8
|
Expo IAP is a powerful in-app purchase solution for Expo and React Native applications that conforms to the Open IAP specification. 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
|
-
If you're shipping an app with expo-iap, we’d love to hear about it—please share your product and feedback in [
|
|
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
|
|
|
12
12
|
<a href="https://openiap.dev"><img src="https://raw.githubusercontent.com/hyodotdev/openiap/main/logo.png" alt="Open IAP" height="40" /></a>
|
|
13
13
|
|
|
@@ -30,19 +30,19 @@ If you're shipping an app with expo-iap, we’d love to hear about it—please s
|
|
|
30
30
|
|
|
31
31
|
## 📚 Documentation
|
|
32
32
|
|
|
33
|
-
**[📖 Visit our comprehensive documentation site →](https://
|
|
33
|
+
**[📖 Visit our comprehensive documentation site →](https://openiap.dev/docs/setup/expo)**
|
|
34
34
|
|
|
35
35
|
## Using with AI Assistants
|
|
36
36
|
|
|
37
37
|
expo-iap provides AI-friendly documentation for Cursor, GitHub Copilot, Claude, and ChatGPT.
|
|
38
38
|
|
|
39
|
-
**[📖 AI Assistants Guide →](https://
|
|
39
|
+
**[📖 AI Assistants Guide →](https://openiap.dev/docs/guides/ai-assistants)**
|
|
40
40
|
|
|
41
41
|
Quick links:
|
|
42
42
|
|
|
43
|
-
- [llms.txt](https://
|
|
44
|
-
- [llms-full.txt](https://
|
|
45
|
-
- [Onside Integration](https://
|
|
43
|
+
- [llms.txt](https://openiap.dev/llms.txt) - Quick reference
|
|
44
|
+
- [llms-full.txt](https://openiap.dev/llms-full.txt) - Full API reference
|
|
45
|
+
- [Onside Integration](https://openiap.dev/docs/features/alternative-marketplace/onside) - Using Onside marketplace payments on iOS
|
|
46
46
|
|
|
47
47
|
## Notice
|
|
48
48
|
|
|
@@ -53,8 +53,7 @@ The `expo-iap` module has been migrated from [react-native-iap](https://github.c
|
|
|
53
53
|
|
|
54
54
|
Both libraries will continue to be maintained in parallel going forward.
|
|
55
55
|
|
|
56
|
-
📖 See the [
|
|
57
|
-
👉 Stay updated via the [Current Project Status comment](https://github.com/hyochan/react-native-iap/discussions/2754#discussioncomment-10510249).
|
|
56
|
+
📖 See the [OpenIAP discussions](https://github.com/hyodotdev/openiap/discussions) for roadmap and project status updates.
|
|
58
57
|
|
|
59
58
|
## Installation
|
|
60
59
|
|
|
@@ -62,7 +61,7 @@ Both libraries will continue to be maintained in parallel going forward.
|
|
|
62
61
|
npx expo install expo-iap
|
|
63
62
|
```
|
|
64
63
|
|
|
65
|
-
For platform-specific configuration (Android Kotlin version, iOS deployment target, etc.), see the [Installation Guide](https://
|
|
64
|
+
For platform-specific configuration (Android Kotlin version, iOS deployment target, etc.), see the [Installation Guide](https://openiap.dev/docs/setup/expo#installation).
|
|
66
65
|
|
|
67
66
|
## Contributing
|
|
68
67
|
|
|
@@ -74,7 +73,7 @@ We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md)
|
|
|
74
73
|
- Code style and conventions
|
|
75
74
|
- Submitting pull requests
|
|
76
75
|
|
|
77
|
-
For detailed usage examples and error handling, see the [documentation](https://
|
|
76
|
+
For detailed usage examples and error handling, see the [documentation](https://openiap.dev/docs/setup/expo).
|
|
78
77
|
|
|
79
78
|
> Sharing your thoughts—any feedback would be greatly appreciated!
|
|
80
79
|
|
|
@@ -107,7 +106,7 @@ For bug reports, please [open an issue](https://github.com/hyodotdev/openiap/iss
|
|
|
107
106
|
|
|
108
107
|
<a href="https://meta.com">
|
|
109
108
|
<div style="display: inline-flex; flex-direction: column; align-items: center; gap: 0.25rem; padding: 0.75rem 1rem; border-radius: 12px; background: rgba(212, 165, 116, 0.12);">
|
|
110
|
-
<img alt="Meta" src="https://
|
|
109
|
+
<img alt="Meta" src="https://openiap.dev/meta.svg" style="width: 120px;" />
|
|
111
110
|
<span style="font-size: 0.85rem; font-weight: 600; color: rgb(107, 78, 61); text-align: center; width: 100%;">Meta</span>
|
|
112
111
|
</div>
|
|
113
112
|
</a>
|
|
@@ -116,9 +115,9 @@ For bug reports, please [open an issue](https://github.com/hyodotdev/openiap/iss
|
|
|
116
115
|
|
|
117
116
|
<div style="display: flex; align-items:center; gap: 10px;">
|
|
118
117
|
<a href="https://namiml.com" style="opacity: 50%">
|
|
119
|
-
<img src="https://
|
|
118
|
+
<img src="https://openiap.dev/sponsors/nami.webp" alt="Nami ML" width="140"/>
|
|
120
119
|
</a>
|
|
121
120
|
<a href="https://www.courier.com/?utm_source=react-native-iap&utm_campaign=osssponsors" style="opacity: 50%;">
|
|
122
|
-
<img width="80" alt="courier_dot_com" src="https://
|
|
121
|
+
<img width="80" alt="courier_dot_com" src="https://openiap.dev/sponsors/courier.webp" />
|
|
123
122
|
</a>
|
|
124
123
|
</div>
|
package/android/build.gradle
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import groovy.json.JsonSlurper
|
|
2
|
+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
2
3
|
|
|
3
4
|
apply plugin: 'com.android.library'
|
|
4
5
|
apply plugin: 'kotlin-android'
|
|
5
6
|
|
|
6
7
|
group = 'expo.modules.iap'
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
def resolvePackageJsonFile() {
|
|
10
|
+
def packageJsonFile = new File(projectDir.parentFile, 'package.json')
|
|
11
|
+
if (!packageJsonFile.isFile()) {
|
|
12
|
+
throw new GradleException("expo-iap: Unable to locate package.json")
|
|
13
|
+
}
|
|
14
|
+
return packageJsonFile
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def expoIapPackageJson = new JsonSlurper().parse(resolvePackageJsonFile())
|
|
18
|
+
def expoIapPackageVersion = (expoIapPackageJson instanceof Map) ? expoIapPackageJson.version : null
|
|
19
|
+
if (!(expoIapPackageVersion instanceof String) || !expoIapPackageVersion.trim()) {
|
|
20
|
+
throw new GradleException("expo-iap: 'version' missing or invalid in package.json")
|
|
21
|
+
}
|
|
22
|
+
expoIapPackageVersion = expoIapPackageVersion.trim()
|
|
23
|
+
version = expoIapPackageVersion
|
|
8
24
|
|
|
9
25
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
10
26
|
apply from: expoModulesCorePlugin
|
|
@@ -32,6 +48,7 @@ if (!(googleVersion instanceof String) || !googleVersion.trim()) {
|
|
|
32
48
|
throw new GradleException("expo-iap: 'google' version missing or invalid in openiap-versions.json")
|
|
33
49
|
}
|
|
34
50
|
def googleVersionString = googleVersion.trim()
|
|
51
|
+
apply from: project.file('openiap-android-sdk.gradle')
|
|
35
52
|
|
|
36
53
|
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
37
54
|
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
@@ -40,26 +57,24 @@ def useManagedAndroidSdkVersions = false
|
|
|
40
57
|
if (useManagedAndroidSdkVersions) {
|
|
41
58
|
useDefaultAndroidSdkVersions()
|
|
42
59
|
} else {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
}
|
|
60
|
+
def openIapCompileSdkVersion = openIapResolveAndroidSdkVersion('compileSdkVersion', 'compileSdk', 35)
|
|
61
|
+
def openIapMinSdkVersion = openIapResolveAndroidSdkVersion('minSdkVersion', 'minSdk', 23)
|
|
62
|
+
def openIapTargetSdkVersion = openIapResolveAndroidSdkVersion('targetSdkVersion', 'compileSdk', 35)
|
|
63
|
+
|
|
49
64
|
project.android {
|
|
50
|
-
|
|
65
|
+
compileSdk = openIapCompileSdkVersion
|
|
51
66
|
defaultConfig {
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
minSdk = openIapMinSdkVersion
|
|
68
|
+
targetSdk = openIapTargetSdkVersion
|
|
54
69
|
}
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
android {
|
|
59
|
-
namespace "expo.modules.iap"
|
|
74
|
+
namespace = "expo.modules.iap"
|
|
60
75
|
defaultConfig {
|
|
61
|
-
versionCode 1
|
|
62
|
-
versionName
|
|
76
|
+
versionCode = 1
|
|
77
|
+
versionName = expoIapPackageVersion
|
|
63
78
|
// When using local openiap-google with flavors, select the appropriate flavor
|
|
64
79
|
// Read horizonEnabled from gradle.properties, default to play
|
|
65
80
|
def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false
|
|
@@ -67,11 +82,7 @@ android {
|
|
|
67
82
|
missingDimensionStrategy "platform", flavor
|
|
68
83
|
}
|
|
69
84
|
lintOptions {
|
|
70
|
-
abortOnError false
|
|
71
|
-
}
|
|
72
|
-
kotlinOptions {
|
|
73
|
-
jvmTarget = "17"
|
|
74
|
-
freeCompilerArgs += ["-Xskip-metadata-version-check"]
|
|
85
|
+
abortOnError = false
|
|
75
86
|
}
|
|
76
87
|
compileOptions {
|
|
77
88
|
sourceCompatibility JavaVersion.VERSION_17
|
|
@@ -79,6 +90,13 @@ android {
|
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
|
|
93
|
+
kotlin {
|
|
94
|
+
compilerOptions {
|
|
95
|
+
jvmTarget.set(JvmTarget.JVM_17)
|
|
96
|
+
freeCompilerArgs.add("-Xskip-metadata-version-check")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
82
100
|
dependencies {
|
|
83
101
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
|
|
84
102
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
ext.openIapReadGoogleAndroidSdkVersion = { String propertyName ->
|
|
2
|
+
File current = projectDir
|
|
3
|
+
while (current != null) {
|
|
4
|
+
File candidate = new File(current, 'packages/google/openiap/build.gradle.kts')
|
|
5
|
+
if (candidate.isFile()) {
|
|
6
|
+
def matcher = candidate.text =~ /(?m)^\s*${propertyName}\s*=\s*(\d+).*$/
|
|
7
|
+
return matcher.find() ? matcher.group(1).toInteger() : null
|
|
8
|
+
}
|
|
9
|
+
current = current.parentFile
|
|
10
|
+
}
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
ext.openIapToIntegerVersion = { Object value, String label ->
|
|
15
|
+
if (value instanceof Number) {
|
|
16
|
+
return value.toInteger()
|
|
17
|
+
}
|
|
18
|
+
if (value instanceof CharSequence && value.toString() ==~ /\d+/) {
|
|
19
|
+
return value.toString().toInteger()
|
|
20
|
+
}
|
|
21
|
+
throw new GradleException("expo-iap: ${label} must be an integer, got ${value}")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ext.openIapResolveAndroidSdkVersion = { String extName, String googlePropertyName, int fallback ->
|
|
25
|
+
if (rootProject.ext.has(extName)) {
|
|
26
|
+
return openIapToIntegerVersion(rootProject.ext.get(extName), extName)
|
|
27
|
+
}
|
|
28
|
+
def googleValue = openIapReadGoogleAndroidSdkVersion(googlePropertyName)
|
|
29
|
+
return googleValue ?: fallback
|
|
30
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package expo.modules.iap
|
|
2
2
|
|
|
3
|
-
import android.util.Log
|
|
4
3
|
import dev.hyo.openiap.AndroidSubscriptionOfferInput
|
|
5
4
|
import dev.hyo.openiap.OpenIapError
|
|
6
5
|
import dev.hyo.openiap.OpenIapModule
|
|
@@ -15,7 +14,6 @@ import java.util.Locale
|
|
|
15
14
|
import java.util.concurrent.ConcurrentLinkedQueue
|
|
16
15
|
|
|
17
16
|
object ExpoIapHelper {
|
|
18
|
-
private const val TAG = "ExpoIapHelper"
|
|
19
17
|
private const val MAX_BUFFERED_EVENTS = 200
|
|
20
18
|
|
|
21
19
|
fun emitOrQueue(
|
|
@@ -67,7 +65,7 @@ object ExpoIapHelper {
|
|
|
67
65
|
val flat = mutableMapOf<String, Any?>()
|
|
68
66
|
// Carry over top-level fields like type, useAlternativeBilling
|
|
69
67
|
for ((k, v) in params) {
|
|
70
|
-
if (k
|
|
68
|
+
if (k != "request") flat[k] = v
|
|
71
69
|
}
|
|
72
70
|
// Overlay platform-specific fields
|
|
73
71
|
for ((k, v) in nested) {
|
|
@@ -205,7 +203,7 @@ object ExpoIapHelper {
|
|
|
205
203
|
runCatching {
|
|
206
204
|
emitOrQueue(module, scope, connectionReady, pendingEvents, eventName, payload)
|
|
207
205
|
}.onFailure { error ->
|
|
208
|
-
|
|
206
|
+
ExpoIapLog.failure("buffer/send $logTag", error)
|
|
209
207
|
val errorPayload =
|
|
210
208
|
mapOf(
|
|
211
209
|
"code" to fallbackErrorCode,
|
|
@@ -213,7 +211,7 @@ object ExpoIapHelper {
|
|
|
213
211
|
)
|
|
214
212
|
runCatching {
|
|
215
213
|
emitOrQueue(module, scope, connectionReady, pendingEvents, eventPurchaseError, errorPayload)
|
|
216
|
-
}.onFailure {
|
|
214
|
+
}.onFailure { ExpoIapLog.failure("send error event", it) }
|
|
217
215
|
}
|
|
218
216
|
}
|
|
219
217
|
|
|
@@ -240,7 +238,7 @@ object ExpoIapHelper {
|
|
|
240
238
|
p.toJson(),
|
|
241
239
|
)
|
|
242
240
|
}.onFailure { error ->
|
|
243
|
-
|
|
241
|
+
ExpoIapLog.failure("buffer/send PURCHASE_UPDATED", error)
|
|
244
242
|
// Emit as purchase error so user knows something went wrong
|
|
245
243
|
val errorPayload =
|
|
246
244
|
mapOf(
|
|
@@ -256,7 +254,7 @@ object ExpoIapHelper {
|
|
|
256
254
|
eventPurchaseError,
|
|
257
255
|
errorPayload,
|
|
258
256
|
)
|
|
259
|
-
}.onFailure {
|
|
257
|
+
}.onFailure { ExpoIapLog.failure("send error event", it) }
|
|
260
258
|
}
|
|
261
259
|
}
|
|
262
260
|
openIap.addPurchaseErrorListener { e ->
|
|
@@ -271,7 +269,7 @@ object ExpoIapHelper {
|
|
|
271
269
|
errorJson,
|
|
272
270
|
)
|
|
273
271
|
}.onFailure { error ->
|
|
274
|
-
|
|
272
|
+
ExpoIapLog.failure("buffer/send PURCHASE_ERROR", error)
|
|
275
273
|
// Critical: if we can't emit the original error, at least try to emit a generic one
|
|
276
274
|
val fallbackPayload =
|
|
277
275
|
mapOf(
|
|
@@ -287,7 +285,7 @@ object ExpoIapHelper {
|
|
|
287
285
|
eventPurchaseError,
|
|
288
286
|
fallbackPayload,
|
|
289
287
|
)
|
|
290
|
-
}.onFailure {
|
|
288
|
+
}.onFailure { ExpoIapLog.failure("send fallback error event", it) }
|
|
291
289
|
}
|
|
292
290
|
// Also reject any pending purchase promises to match iOS behavior
|
|
293
291
|
val errorCode = errorJson["code"] as? String ?: OpenIapError.PurchaseFailed.CODE
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package expo.modules.iap
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
-
import android.util.Log
|
|
5
4
|
import dev.hyo.openiap.AndroidSubscriptionOfferInput
|
|
6
5
|
import dev.hyo.openiap.DeepLinkOptions
|
|
7
6
|
import dev.hyo.openiap.FetchProductsResultProducts
|
|
@@ -143,7 +142,7 @@ class ExpoIapModule : Module() {
|
|
|
143
142
|
val ev = pendingEvents.poll() ?: break
|
|
144
143
|
// Already on main dispatcher here; emit directly
|
|
145
144
|
runCatching { sendEvent(ev.first, ev.second) }
|
|
146
|
-
.onFailure {
|
|
145
|
+
.onFailure { ExpoIapLog.failure("flush buffered event ${ev.first}", it) }
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
ExpoIapLog.result("initConnection", true)
|
|
@@ -354,11 +353,7 @@ class ExpoIapModule : Module() {
|
|
|
354
353
|
errorMap,
|
|
355
354
|
)
|
|
356
355
|
}.onFailure { ex ->
|
|
357
|
-
|
|
358
|
-
TAG,
|
|
359
|
-
"Failed to send PURCHASE_ERROR event (requestPurchase)",
|
|
360
|
-
ex,
|
|
361
|
-
)
|
|
356
|
+
ExpoIapLog.failure("send PURCHASE_ERROR event requestPurchase", ex)
|
|
362
357
|
}
|
|
363
358
|
ExpoIapHelper.rejectPurchasePromises(
|
|
364
359
|
errorCode,
|
|
@@ -391,7 +386,10 @@ class ExpoIapModule : Module() {
|
|
|
391
386
|
try {
|
|
392
387
|
openIap.consumePurchaseAndroid(token)
|
|
393
388
|
val response = mapOf("responseCode" to 0, "purchaseToken" to token)
|
|
394
|
-
ExpoIapLog.result(
|
|
389
|
+
ExpoIapLog.result(
|
|
390
|
+
"consumePurchaseAndroid",
|
|
391
|
+
response,
|
|
392
|
+
)
|
|
395
393
|
promise.resolve(response)
|
|
396
394
|
} catch (e: Exception) {
|
|
397
395
|
ExpoIapLog.failure("consumePurchaseAndroid", e)
|
|
@@ -423,7 +421,7 @@ class ExpoIapModule : Module() {
|
|
|
423
421
|
val activity =
|
|
424
422
|
runCatching { currentActivity }
|
|
425
423
|
.onFailure {
|
|
426
|
-
|
|
424
|
+
ExpoIapLog.failure("showAlternativeBillingDialogAndroid activity", it)
|
|
427
425
|
}.getOrNull() ?: run {
|
|
428
426
|
promise.reject(OpenIapError.ServiceUnavailable.CODE, "Activity not available", null)
|
|
429
427
|
return@launch
|
|
@@ -587,7 +585,10 @@ class ExpoIapModule : Module() {
|
|
|
587
585
|
"billingProgram" to program,
|
|
588
586
|
"externalTransactionToken" to result.externalTransactionToken,
|
|
589
587
|
)
|
|
590
|
-
ExpoIapLog.result(
|
|
588
|
+
ExpoIapLog.result(
|
|
589
|
+
"createBillingProgramReportingDetailsAndroid",
|
|
590
|
+
response,
|
|
591
|
+
)
|
|
591
592
|
promise.resolve(response)
|
|
592
593
|
} catch (e: Exception) {
|
|
593
594
|
ExpoIapLog.failure("createBillingProgramReportingDetailsAndroid", e)
|
|
@@ -603,7 +604,7 @@ class ExpoIapModule : Module() {
|
|
|
603
604
|
val activity =
|
|
604
605
|
runCatching { currentActivity }
|
|
605
606
|
.onFailure {
|
|
606
|
-
|
|
607
|
+
ExpoIapLog.failure("launchExternalLinkAndroid activity", it)
|
|
607
608
|
}.getOrNull() ?: run {
|
|
608
609
|
promise.reject(OpenIapError.ServiceUnavailable.CODE, "Activity not available", null)
|
|
609
610
|
return@launch
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package expo.modules.iap
|
|
2
2
|
|
|
3
|
-
import android.util.Log
|
|
4
3
|
import dev.hyo.openiap.OpenIapError
|
|
5
4
|
import expo.modules.kotlin.Promise
|
|
6
5
|
|
|
@@ -54,7 +53,7 @@ fun Promise.safeResolve(value: Any?) {
|
|
|
54
53
|
try {
|
|
55
54
|
this.resolve(value)
|
|
56
55
|
} catch (e: RuntimeException) {
|
|
57
|
-
|
|
56
|
+
ExpoIapLog.debug("Already consumed ${e.message}")
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
|
|
@@ -78,6 +77,6 @@ fun Promise.safeReject(
|
|
|
78
77
|
try {
|
|
79
78
|
this.reject(code, message, throwable)
|
|
80
79
|
} catch (e: RuntimeException) {
|
|
81
|
-
|
|
80
|
+
ExpoIapLog.debug("Already consumed ${e.message}")
|
|
82
81
|
}
|
|
83
82
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoIapModule.d.ts","sourceRoot":"","sources":["../src/ExpoIapModule.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ExpoIapModule.d.ts","sourceRoot":"","sources":["../src/ExpoIapModule.ts"],"names":[],"mappings":"AAsEA,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAQtD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,eAAe,QAE9B;;AAoBD,wBAeG"}
|
package/build/ExpoIapModule.js
CHANGED
|
@@ -1,30 +1,47 @@
|
|
|
1
1
|
import { requireNativeModule, UnavailabilityError } from 'expo-modules-core';
|
|
2
2
|
import { installedFromOnside } from './onside';
|
|
3
|
+
const ONSIDE_MARKETPLACE_ID = 'com.onside.marketplace-app';
|
|
3
4
|
let cached = null;
|
|
5
|
+
let expoIapFallback;
|
|
6
|
+
let onsideModuleUnavailable = false;
|
|
7
|
+
function isOnsideInstallation() {
|
|
8
|
+
if (installedFromOnside === true) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
if (typeof installedFromOnside !== 'string') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const normalized = installedFromOnside.trim().toLowerCase();
|
|
15
|
+
return normalized === 'true' || normalized === ONSIDE_MARKETPLACE_ID;
|
|
16
|
+
}
|
|
17
|
+
function shouldUseOnsideModule() {
|
|
18
|
+
return isOnsideInstallation() && !onsideModuleUnavailable;
|
|
19
|
+
}
|
|
4
20
|
function getResolved() {
|
|
5
|
-
|
|
21
|
+
const expectedName = shouldUseOnsideModule()
|
|
22
|
+
? 'ExpoIapOnside'
|
|
23
|
+
: 'ExpoIap';
|
|
24
|
+
if (!cached || cached.name !== expectedName) {
|
|
6
25
|
cached = resolveNativeModule();
|
|
7
26
|
}
|
|
8
27
|
return cached;
|
|
9
28
|
}
|
|
10
29
|
function resolveNativeModule() {
|
|
11
|
-
|
|
12
|
-
for (const name of candidates) {
|
|
30
|
+
if (isOnsideInstallation()) {
|
|
13
31
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
return { module, name };
|
|
32
|
+
return {
|
|
33
|
+
module: requireNativeModule('ExpoIapOnside'),
|
|
34
|
+
name: 'ExpoIapOnside',
|
|
35
|
+
};
|
|
19
36
|
}
|
|
20
37
|
catch (error) {
|
|
21
|
-
if (
|
|
22
|
-
|
|
38
|
+
if (!isMissingModuleError(error, 'ExpoIapOnside')) {
|
|
39
|
+
throw error;
|
|
23
40
|
}
|
|
24
|
-
|
|
41
|
+
onsideModuleUnavailable = true;
|
|
25
42
|
}
|
|
26
43
|
}
|
|
27
|
-
|
|
44
|
+
return { module: requireNativeModule('ExpoIap'), name: 'ExpoIap' };
|
|
28
45
|
}
|
|
29
46
|
function isMissingModuleError(error, moduleName) {
|
|
30
47
|
if (error instanceof UnavailabilityError) {
|
|
@@ -51,14 +68,36 @@ export const NATIVE_ERROR_CODES = new Proxy({}, {
|
|
|
51
68
|
export function getNativeModule() {
|
|
52
69
|
return getResolved().module;
|
|
53
70
|
}
|
|
71
|
+
function getExpoIapFallbackModule() {
|
|
72
|
+
if (expoIapFallback !== undefined) {
|
|
73
|
+
return expoIapFallback;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
expoIapFallback = requireNativeModule('ExpoIap');
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (isMissingModuleError(error, 'ExpoIap')) {
|
|
80
|
+
expoIapFallback = null;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return expoIapFallback;
|
|
87
|
+
}
|
|
54
88
|
export default new Proxy({}, {
|
|
55
89
|
get(target, prop) {
|
|
56
90
|
if (typeof prop === 'symbol')
|
|
57
91
|
return Reflect.get(target, prop);
|
|
92
|
+
const resolved = getResolved();
|
|
58
93
|
if (prop === 'USING_ONSIDE_SDK') {
|
|
59
|
-
return
|
|
94
|
+
return resolved.name === 'ExpoIapOnside';
|
|
95
|
+
}
|
|
96
|
+
const value = resolved.module[prop];
|
|
97
|
+
if (value !== undefined || resolved.name !== 'ExpoIapOnside') {
|
|
98
|
+
return value;
|
|
60
99
|
}
|
|
61
|
-
return
|
|
100
|
+
return getExpoIapFallbackModule()?.[prop];
|
|
62
101
|
},
|
|
63
102
|
});
|
|
64
103
|
//# sourceMappingURL=ExpoIapModule.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoIapModule.js","sourceRoot":"","sources":["../src/ExpoIapModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,mBAAmB,EAAE,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EAAC,mBAAmB,EAAC,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"ExpoIapModule.js","sourceRoot":"","sources":["../src/ExpoIapModule.ts"],"names":[],"mappings":"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","sourcesContent":["import {requireNativeModule, UnavailabilityError} from 'expo-modules-core';\nimport {installedFromOnside} from './onside';\n\ntype NativeIapModuleName = 'ExpoIapOnside' | 'ExpoIap';\nconst ONSIDE_MARKETPLACE_ID = 'com.onside.marketplace-app';\n\nlet cached: {module: any; name: NativeIapModuleName} | null = null;\nlet expoIapFallback: any | null | undefined;\nlet onsideModuleUnavailable = false;\n\nfunction isOnsideInstallation(): boolean {\n if (installedFromOnside === true) {\n return true;\n }\n\n if (typeof installedFromOnside !== 'string') {\n return false;\n }\n\n const normalized = installedFromOnside.trim().toLowerCase();\n return normalized === 'true' || normalized === ONSIDE_MARKETPLACE_ID;\n}\n\nfunction shouldUseOnsideModule(): boolean {\n return isOnsideInstallation() && !onsideModuleUnavailable;\n}\n\nfunction getResolved(): {module: any; name: NativeIapModuleName} {\n const expectedName: NativeIapModuleName = shouldUseOnsideModule()\n ? 'ExpoIapOnside'\n : 'ExpoIap';\n if (!cached || cached.name !== expectedName) {\n cached = resolveNativeModule();\n }\n return cached;\n}\n\nfunction resolveNativeModule(): {\n module: any;\n name: NativeIapModuleName;\n} {\n if (isOnsideInstallation()) {\n try {\n return {\n module: requireNativeModule('ExpoIapOnside'),\n name: 'ExpoIapOnside',\n };\n } catch (error) {\n if (!isMissingModuleError(error, 'ExpoIapOnside')) {\n throw error;\n }\n onsideModuleUnavailable = true;\n }\n }\n\n return {module: requireNativeModule('ExpoIap'), name: 'ExpoIap'};\n}\n\nfunction isMissingModuleError(error: unknown, moduleName: string): boolean {\n if (error instanceof UnavailabilityError) {\n return true;\n }\n\n if (error instanceof Error) {\n return error.message.includes(`Cannot find native module '${moduleName}'`);\n }\n\n return false;\n}\n\nexport const NATIVE_ERROR_CODES: Record<string, unknown> = new Proxy(\n {} as Record<string, unknown>,\n {\n get(target, prop) {\n if (typeof prop === 'symbol') return Reflect.get(target, prop);\n return (getResolved().module.ERROR_CODES || {})[prop as string];\n },\n },\n);\n\n/**\n * Returns the raw native module (not wrapped in a Proxy).\n * Use this for EventEmitter / addListener calls — JSI HostObjects\n * require the real native module as `this`; a Proxy triggers\n * \"native state unsupported on Proxy\" on New Architecture / Hermes.\n */\nexport function getNativeModule() {\n return getResolved().module;\n}\n\nfunction getExpoIapFallbackModule(): any | null {\n if (expoIapFallback !== undefined) {\n return expoIapFallback;\n }\n\n try {\n expoIapFallback = requireNativeModule('ExpoIap');\n } catch (error) {\n if (isMissingModuleError(error, 'ExpoIap')) {\n expoIapFallback = null;\n } else {\n throw error;\n }\n }\n\n return expoIapFallback;\n}\n\nexport default new Proxy({} as any, {\n get(target, prop) {\n if (typeof prop === 'symbol') return Reflect.get(target, prop);\n const resolved = getResolved();\n if (prop === 'USING_ONSIDE_SDK') {\n return resolved.name === 'ExpoIapOnside';\n }\n\n const value = resolved.module[prop];\n if (value !== undefined || resolved.name !== 'ExpoIapOnside') {\n return value;\n }\n\n return getExpoIapFallbackModule()?.[prop];\n },\n});\n"]}
|