expo-iap 3.0.8 → 3.1.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 +2 -2
- package/CONTRIBUTING.md +19 -0
- package/README.md +18 -6
- package/android/build.gradle +24 -1
- package/android/src/main/java/expo/modules/iap/ExpoIapLog.kt +69 -0
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +190 -59
- package/build/index.d.ts +20 -47
- package/build/index.d.ts.map +1 -1
- package/build/index.js +94 -137
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +2 -1
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +16 -1
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +29 -16
- package/build/modules/ios.js.map +1 -1
- package/build/types.d.ts +8 -6
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/useIAP.d.ts +1 -1
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +12 -15
- package/build/useIAP.js.map +1 -1
- package/build/utils/errorMapping.d.ts +32 -23
- package/build/utils/errorMapping.d.ts.map +1 -1
- package/build/utils/errorMapping.js +117 -22
- package/build/utils/errorMapping.js.map +1 -1
- package/ios/ExpoIap.podspec +3 -2
- package/ios/ExpoIapHelper.swift +96 -0
- package/ios/ExpoIapLog.swift +127 -0
- package/ios/ExpoIapModule.swift +218 -340
- package/openiap-versions.json +5 -0
- package/package.json +2 -2
- package/plugin/build/withIAP.js +6 -4
- package/plugin/src/withIAP.ts +14 -4
- package/scripts/update-types.mjs +20 -1
- package/src/index.ts +122 -165
- package/src/modules/android.ts +2 -1
- package/src/modules/ios.ts +31 -19
- package/src/types.ts +8 -6
- package/src/useIAP.ts +17 -25
- package/src/utils/errorMapping.ts +203 -23
- package/build/purchase-error.d.ts +0 -67
- package/build/purchase-error.d.ts.map +0 -1
- package/build/purchase-error.js +0 -166
- package/build/purchase-error.js.map +0 -1
- package/build/utils/purchase.d.ts +0 -9
- package/build/utils/purchase.d.ts.map +0 -1
- package/build/utils/purchase.js +0 -34
- package/build/utils/purchase.js.map +0 -1
- package/src/purchase-error.ts +0 -265
- package/src/utils/purchase.ts +0 -52
package/CLAUDE.md
CHANGED
|
@@ -93,7 +93,7 @@ The library follows the OpenIAP type specifications with platform-specific exten
|
|
|
93
93
|
### Hook API Semantics (useIAP)
|
|
94
94
|
|
|
95
95
|
- Inside the `useIAP` hook, most methods return `Promise<void>` and update internal state. Do not design examples or implementations that expect data from these methods.
|
|
96
|
-
- Examples: `fetchProducts`, `
|
|
96
|
+
- Examples: `fetchProducts`, `requestPurchase`, `getAvailablePurchases`.
|
|
97
97
|
- After calling, consume state from the hook: `products`, `subscriptions`, `availablePurchases`, etc.
|
|
98
98
|
- Defined exceptions that DO return values in the hook:
|
|
99
99
|
- `getActiveSubscriptions(subscriptionIds?) => Promise<ActiveSubscription[]>` (also updates `activeSubscriptions` state)
|
|
@@ -102,7 +102,7 @@ The library follows the OpenIAP type specifications with platform-specific exten
|
|
|
102
102
|
|
|
103
103
|
### API Method Naming
|
|
104
104
|
|
|
105
|
-
- Functions that depend on event results should use `request` prefix (e.g., `requestPurchase
|
|
105
|
+
- Functions that depend on event results should use `request` prefix (e.g., `requestPurchase`)
|
|
106
106
|
- Follow OpenIAP terminology: <https://www.openiap.dev/docs/apis#terminology>
|
|
107
107
|
- Do not use generic prefixes like `get`, `find` - refer to the official terminology
|
|
108
108
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -8,6 +8,7 @@ Thank you for your interest in contributing to expo-iap! This guide will help yo
|
|
|
8
8
|
- [Package Manager](#-package-manager)
|
|
9
9
|
- [Running the Example App](#-running-the-example-app)
|
|
10
10
|
- [Development Guidelines](#-development-guidelines)
|
|
11
|
+
- [OpenIAP Version Management](#-openiap-version-management)
|
|
11
12
|
- [Testing](#-testing)
|
|
12
13
|
- [Code Style](#-code-style)
|
|
13
14
|
- [Submitting Changes](#-submitting-changes)
|
|
@@ -223,6 +224,24 @@ The generated TypeScript definitions in `src/types.ts` come from the [`openiap-g
|
|
|
223
224
|
|
|
224
225
|
Always ensure the repository builds and tests succeed after regenerating the types.
|
|
225
226
|
|
|
227
|
+
## 🔢 OpenIAP Version Management
|
|
228
|
+
|
|
229
|
+
All native and type-generation version numbers are sourced from `openiap-versions.json` at the repository root:
|
|
230
|
+
|
|
231
|
+
- `apple` → iOS Pod dependency (`ios/ExpoIap.podspec`).
|
|
232
|
+
- `google` → Android artifact (`android/build.gradle`, Expo config plugin).
|
|
233
|
+
- `gql` → GraphQL type generator (`scripts/update-types.mjs`).
|
|
234
|
+
|
|
235
|
+
When bumping dependencies:
|
|
236
|
+
|
|
237
|
+
1. Update the relevant fields in `openiap-versions.json`.
|
|
238
|
+
2. For iOS changes, run `cd ios && pod install` (and commit the Pod.lock if required by the workflow).
|
|
239
|
+
3. For Android, re-run Gradle (`bun run android`) so the new artifact is pulled down.
|
|
240
|
+
4. When the `gql` value changes, run `bun run generate:types` to refresh `src/types.ts`.
|
|
241
|
+
5. Commit the updated JSON, regenerated files, and any resulting lockfile changes together.
|
|
242
|
+
|
|
243
|
+
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.
|
|
244
|
+
|
|
226
245
|
### Development Workflow
|
|
227
246
|
|
|
228
247
|
1. **Before starting work**:
|
package/README.md
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
[](https://npmjs.org/package/expo-iap) [](https://npmjs.org/package/expo-iap) [](https://github.com/hyochan/expo-iap/actions/workflows/ci.yml) [](https://codecov.io/gh/hyochan/expo-iap) [](https://app.fossa.com/projects/git%2Bgithub.com%2Fhyochan%2Fexpo-iap?ref=badge_shield&issueType=license)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
|
|
10
|
+
If you're shipping an app with expo-iap, we’d love to hear about it—please share your product and feedback in [Who's using Expo IAP?](https://github.com/hyochan/expo-iap/discussions/143). Community stories help us keep improving the ecosystem.
|
|
11
|
+
|
|
12
|
+
<a href="https://openiap.dev"><img src="https://openiap.dev/logo.png" alt="Open IAP" height="40" /></a>
|
|
13
|
+
|
|
11
14
|
</div>
|
|
12
15
|
|
|
13
16
|
## 📚 Documentation
|
|
@@ -44,7 +47,7 @@ npx expo install expo-iap
|
|
|
44
47
|
"expo-build-properties",
|
|
45
48
|
{
|
|
46
49
|
"android": {
|
|
47
|
-
"kotlinVersion": "2.
|
|
50
|
+
"kotlinVersion": "2.1.20"
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
]
|
|
@@ -53,6 +56,8 @@ npx expo install expo-iap
|
|
|
53
56
|
}
|
|
54
57
|
```
|
|
55
58
|
|
|
59
|
+
If you're targeting Expo SDK 54 or newer, please confirm whether this manual override is still required and share findings with the community at [github.com/hyochan/expo-iap/discussions](https://github.com/hyochan/expo-iap/discussions).
|
|
60
|
+
|
|
56
61
|
## Contributing
|
|
57
62
|
|
|
58
63
|
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details on:
|
|
@@ -67,14 +72,21 @@ For detailed usage examples and error handling, see the [documentation](https://
|
|
|
67
72
|
|
|
68
73
|
> Sharing your thoughts—any feedback would be greatly appreciated!
|
|
69
74
|
|
|
70
|
-
## Sponsors
|
|
75
|
+
## Our Sponsors
|
|
71
76
|
|
|
72
77
|
💼 **[View Our Sponsors](https://openiap.dev/sponsors)**
|
|
73
78
|
|
|
79
|
+
We're building the OpenIAP ecosystem—defining the spec at [openiap.dev](https://www.openiap.dev), maintaining [openiap-gql](https://github.com/hyodotdev/openiap-gql) for the shared type system, and shipping platform SDKs like [openiap-apple](https://github.com/hyodotdev/openiap-apple) and [openiap-google](https://github.com/hyodotdev/openiap-google) that power [expo-iap](https://github.com/hyochan/expo-iap), [flutter_inapp_purchase](https://github.com/hyochan/flutter_inapp_purchase), React Native, and [kmp-iap](https://github.com/hyochan/kmp-iap). The work so far has focused on untangling fragmented APIs; the next milestone is a streamlined purchase flow: `initConnection → fetchProducts → requestPurchase → (server receipt validation) → finishTransaction`.
|
|
80
|
+
|
|
81
|
+
Your sponsorship helps ensure developers across platforms, OS, and frameworks can implement in-app purchases without headaches. It also fuels new plugins, payment systems, and partner integrations already being explored in the OpenIAP community. Sponsors receive shout-outs in every release and can request tailored support depending on tier. If you’re interested—or have rollout feedback to share—you can view sponsorship options at [openiap.dev/sponsors](https://openiap.dev/sponsors).
|
|
82
|
+
|
|
74
83
|
### <p style="color: rgb(255, 182, 193);">Angel</p>
|
|
75
84
|
|
|
76
85
|
<a href="https://meta.com">
|
|
77
|
-
<
|
|
86
|
+
<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);">
|
|
87
|
+
<img alt="Meta" src="https://www.openiap.dev/meta.svg" style="width: 120px;" />
|
|
88
|
+
<span style="font-size: 0.85rem; font-weight: 600; color: rgb(107, 78, 61); text-align: center; width: 100%;">Meta</span>
|
|
89
|
+
</div>
|
|
78
90
|
</a>
|
|
79
91
|
|
|
80
92
|
## Past Supporters
|
package/android/build.gradle
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import groovy.json.JsonSlurper
|
|
2
|
+
|
|
1
3
|
apply plugin: 'com.android.library'
|
|
2
4
|
apply plugin: 'kotlin-android'
|
|
3
5
|
|
|
@@ -10,6 +12,27 @@ applyKotlinExpoModulesCorePlugin()
|
|
|
10
12
|
useCoreDependencies()
|
|
11
13
|
useExpoPublishing()
|
|
12
14
|
|
|
15
|
+
def resolveOpenIapVersionsFile() {
|
|
16
|
+
def candidates = [
|
|
17
|
+
new File(projectDir.parentFile, 'openiap-versions.json'),
|
|
18
|
+
new File(rootDir.parentFile ?: rootDir, 'openiap-versions.json'),
|
|
19
|
+
new File(rootProject.projectDir.parentFile ?: rootProject.projectDir, 'openiap-versions.json')
|
|
20
|
+
]
|
|
21
|
+
return candidates.find { it.exists() }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def openiapVersionsFile = resolveOpenIapVersionsFile()
|
|
25
|
+
if (openiapVersionsFile == null) {
|
|
26
|
+
throw new GradleException("expo-iap: Unable to locate openiap-versions.json")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def openiapVersions = new JsonSlurper().parse(openiapVersionsFile)
|
|
30
|
+
def googleVersion = (openiapVersions instanceof Map) ? openiapVersions.google : null
|
|
31
|
+
if (!(googleVersion instanceof String) || !googleVersion.trim()) {
|
|
32
|
+
throw new GradleException("expo-iap: 'google' version missing or invalid in openiap-versions.json")
|
|
33
|
+
}
|
|
34
|
+
def googleVersionString = googleVersion.trim()
|
|
35
|
+
|
|
13
36
|
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
14
37
|
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
15
38
|
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
@@ -58,6 +81,6 @@ dependencies {
|
|
|
58
81
|
implementation project(":openiap-google")
|
|
59
82
|
} else {
|
|
60
83
|
// Fallback to published artifact when local project isn't linked
|
|
61
|
-
implementation "io.github.hyochan.openiap:openiap-google
|
|
84
|
+
implementation "io.github.hyochan.openiap:openiap-google:${googleVersionString}"
|
|
62
85
|
}
|
|
63
86
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package expo.modules.iap
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import org.json.JSONArray
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
|
|
7
|
+
internal object ExpoIapLog {
|
|
8
|
+
private const val TAG = "ExpoIap"
|
|
9
|
+
|
|
10
|
+
fun payload(
|
|
11
|
+
name: String,
|
|
12
|
+
payload: Any?,
|
|
13
|
+
) {
|
|
14
|
+
debug("$name payload: ${stringify(payload)}")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fun result(
|
|
18
|
+
name: String,
|
|
19
|
+
value: Any?,
|
|
20
|
+
) {
|
|
21
|
+
debug("$name result: ${stringify(value)}")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fun failure(
|
|
25
|
+
name: String,
|
|
26
|
+
error: Throwable,
|
|
27
|
+
) {
|
|
28
|
+
Log.e(TAG, "$name failed: ${error.localizedMessage}", error)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun debug(message: String) {
|
|
32
|
+
Log.d(TAG, message)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private fun stringify(value: Any?): String {
|
|
36
|
+
val sanitized = sanitize(value) ?: return "null"
|
|
37
|
+
return when (sanitized) {
|
|
38
|
+
is String -> sanitized
|
|
39
|
+
is Number, is Boolean -> sanitized.toString()
|
|
40
|
+
is Map<*, *> -> JSONObject(sanitized).toString()
|
|
41
|
+
is List<*> -> JSONArray(sanitized).toString()
|
|
42
|
+
else -> sanitized.toString()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private fun sanitize(value: Any?): Any? {
|
|
47
|
+
if (value == null) return null
|
|
48
|
+
|
|
49
|
+
return when (value) {
|
|
50
|
+
is Map<*, *> -> sanitizeMap(value)
|
|
51
|
+
is List<*> -> value.mapNotNull { sanitize(it) }
|
|
52
|
+
is Array<*> -> value.mapNotNull { sanitize(it) }
|
|
53
|
+
else -> value
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private fun sanitizeMap(source: Map<*, *>): Map<String, Any?> {
|
|
58
|
+
val sanitized = linkedMapOf<String, Any?>()
|
|
59
|
+
for ((rawKey, rawValue) in source) {
|
|
60
|
+
val key = rawKey as? String ?: continue
|
|
61
|
+
if (key.lowercase().contains("token")) {
|
|
62
|
+
sanitized[key] = "hidden"
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
sanitized[key] = sanitize(rawValue)
|
|
66
|
+
}
|
|
67
|
+
return sanitized
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -2,12 +2,22 @@ package expo.modules.iap
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.util.Log
|
|
5
|
+
import dev.hyo.openiap.AndroidSubscriptionOfferInput
|
|
6
|
+
import dev.hyo.openiap.DeepLinkOptions
|
|
7
|
+
import dev.hyo.openiap.FetchProductsResultProducts
|
|
8
|
+
import dev.hyo.openiap.FetchProductsResultSubscriptions
|
|
5
9
|
import dev.hyo.openiap.OpenIapError
|
|
6
10
|
import dev.hyo.openiap.OpenIapModule
|
|
7
|
-
import dev.hyo.openiap.
|
|
8
|
-
import dev.hyo.openiap.
|
|
9
|
-
import dev.hyo.openiap.
|
|
10
|
-
import dev.hyo.openiap.
|
|
11
|
+
import dev.hyo.openiap.ProductQueryType
|
|
12
|
+
import dev.hyo.openiap.ProductRequest
|
|
13
|
+
import dev.hyo.openiap.Purchase
|
|
14
|
+
import dev.hyo.openiap.RequestPurchaseAndroidProps
|
|
15
|
+
import dev.hyo.openiap.RequestPurchaseProps
|
|
16
|
+
import dev.hyo.openiap.RequestPurchasePropsByPlatforms
|
|
17
|
+
import dev.hyo.openiap.RequestPurchaseResultPurchase
|
|
18
|
+
import dev.hyo.openiap.RequestPurchaseResultPurchases
|
|
19
|
+
import dev.hyo.openiap.RequestSubscriptionAndroidProps
|
|
20
|
+
import dev.hyo.openiap.RequestSubscriptionPropsByPlatforms
|
|
11
21
|
import expo.modules.kotlin.Promise
|
|
12
22
|
import expo.modules.kotlin.exception.Exceptions
|
|
13
23
|
import expo.modules.kotlin.modules.Module
|
|
@@ -18,6 +28,7 @@ import kotlinx.coroutines.Job
|
|
|
18
28
|
import kotlinx.coroutines.launch
|
|
19
29
|
import kotlinx.coroutines.sync.Mutex
|
|
20
30
|
import kotlinx.coroutines.sync.withLock
|
|
31
|
+
import java.util.Locale
|
|
21
32
|
import java.util.concurrent.ConcurrentLinkedQueue
|
|
22
33
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
23
34
|
|
|
@@ -59,7 +70,20 @@ class ExpoIapModule : Module() {
|
|
|
59
70
|
pendingEvents.add(name to payload)
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
|
|
73
|
+
private fun parseProductQueryType(rawType: String?): ProductQueryType {
|
|
74
|
+
val normalized =
|
|
75
|
+
rawType
|
|
76
|
+
?.trim()
|
|
77
|
+
?.lowercase(Locale.US)
|
|
78
|
+
?.replace("-", "")
|
|
79
|
+
?.replace("_", "")
|
|
80
|
+
|
|
81
|
+
return when (normalized) {
|
|
82
|
+
"subs" -> ProductQueryType.Subs
|
|
83
|
+
"all" -> ProductQueryType.All
|
|
84
|
+
else -> ProductQueryType.InApp
|
|
85
|
+
}
|
|
86
|
+
}
|
|
63
87
|
|
|
64
88
|
override fun definition() =
|
|
65
89
|
ModuleDefinition {
|
|
@@ -72,6 +96,7 @@ class ExpoIapModule : Module() {
|
|
|
72
96
|
Events(EVENT_PURCHASE_UPDATED, EVENT_PURCHASE_ERROR)
|
|
73
97
|
|
|
74
98
|
AsyncFunction("initConnection") { promise: Promise ->
|
|
99
|
+
ExpoIapLog.payload("initConnection", null)
|
|
75
100
|
scope.launch {
|
|
76
101
|
connectionMutex.withLock {
|
|
77
102
|
try {
|
|
@@ -82,6 +107,7 @@ class ExpoIapModule : Module() {
|
|
|
82
107
|
|
|
83
108
|
// If already connected, short-circuit
|
|
84
109
|
if (connectionReady.get()) {
|
|
110
|
+
ExpoIapLog.result("initConnection", true)
|
|
85
111
|
promise.resolve(true)
|
|
86
112
|
return@withLock
|
|
87
113
|
}
|
|
@@ -90,8 +116,9 @@ class ExpoIapModule : Module() {
|
|
|
90
116
|
if (!listenersAttached) {
|
|
91
117
|
listenersAttached = true
|
|
92
118
|
openIap.addPurchaseUpdateListener { p ->
|
|
93
|
-
runCatching {
|
|
94
|
-
|
|
119
|
+
runCatching {
|
|
120
|
+
emitOrQueue(EVENT_PURCHASE_UPDATED, p.toJson())
|
|
121
|
+
}.onFailure { Log.e(TAG, "Failed to buffer/send PURCHASE_UPDATED", it) }
|
|
95
122
|
}
|
|
96
123
|
openIap.addPurchaseErrorListener { e ->
|
|
97
124
|
runCatching { emitOrQueue(EVENT_PURCHASE_ERROR, e.toJSON()) }
|
|
@@ -104,6 +131,10 @@ class ExpoIapModule : Module() {
|
|
|
104
131
|
if (!ok) {
|
|
105
132
|
// Clear any buffered events from a failed init
|
|
106
133
|
pendingEvents.clear()
|
|
134
|
+
ExpoIapLog.failure(
|
|
135
|
+
"initConnection",
|
|
136
|
+
IllegalStateException("Failed to initialize connection"),
|
|
137
|
+
)
|
|
107
138
|
promise.reject(OpenIapError.InitConnection.CODE, "Failed to initialize connection", null)
|
|
108
139
|
return@withLock
|
|
109
140
|
}
|
|
@@ -117,8 +148,10 @@ class ExpoIapModule : Module() {
|
|
|
117
148
|
.onFailure { Log.e(TAG, "Failed to flush buffered event: ${ev.first}", it) }
|
|
118
149
|
}
|
|
119
150
|
|
|
151
|
+
ExpoIapLog.result("initConnection", true)
|
|
120
152
|
promise.resolve(true)
|
|
121
153
|
} catch (e: Exception) {
|
|
154
|
+
ExpoIapLog.failure("initConnection", e)
|
|
122
155
|
promise.reject(OpenIapError.InitConnection.CODE, e.message, e)
|
|
123
156
|
}
|
|
124
157
|
}
|
|
@@ -126,35 +159,54 @@ class ExpoIapModule : Module() {
|
|
|
126
159
|
}
|
|
127
160
|
|
|
128
161
|
AsyncFunction("endConnection") { promise: Promise ->
|
|
162
|
+
ExpoIapLog.payload("endConnection", null)
|
|
129
163
|
scope.launch {
|
|
130
164
|
connectionMutex.withLock {
|
|
131
165
|
runCatching { openIap.endConnection() }
|
|
132
166
|
// Reset connection state and clear any buffered events
|
|
133
167
|
connectionReady.set(false)
|
|
134
168
|
pendingEvents.clear()
|
|
169
|
+
ExpoIapLog.result("endConnection", true)
|
|
135
170
|
promise.resolve(true)
|
|
136
171
|
}
|
|
137
172
|
}
|
|
138
173
|
}
|
|
139
174
|
|
|
140
175
|
AsyncFunction("fetchProducts") { type: String, skuArr: Array<String>, promise: Promise ->
|
|
176
|
+
ExpoIapLog.payload(
|
|
177
|
+
"fetchProductsAndroid",
|
|
178
|
+
mapOf("type" to type, "skus" to skuArr.toList()),
|
|
179
|
+
)
|
|
141
180
|
scope.launch {
|
|
142
181
|
try {
|
|
143
|
-
val
|
|
144
|
-
val
|
|
145
|
-
|
|
182
|
+
val queryType = parseProductQueryType(type)
|
|
183
|
+
val request = ProductRequest(skuArr.toList(), queryType)
|
|
184
|
+
val result = openIap.fetchProducts(request)
|
|
185
|
+
val payload =
|
|
186
|
+
when (result) {
|
|
187
|
+
is FetchProductsResultProducts -> result.value.orEmpty().map { it.toJson() }
|
|
188
|
+
is FetchProductsResultSubscriptions -> result.value.orEmpty().map { it.toJson() }
|
|
189
|
+
else -> emptyList<Map<String, Any?>>()
|
|
190
|
+
}
|
|
191
|
+
ExpoIapLog.result("fetchProductsAndroid", payload)
|
|
192
|
+
promise.resolve(payload)
|
|
146
193
|
} catch (e: Exception) {
|
|
194
|
+
ExpoIapLog.failure("fetchProductsAndroid", e)
|
|
147
195
|
promise.reject(OpenIapError.QueryProduct.CODE, e.message, null)
|
|
148
196
|
}
|
|
149
197
|
}
|
|
150
198
|
}
|
|
151
199
|
|
|
152
200
|
AsyncFunction("getAvailableItems") { promise: Promise ->
|
|
201
|
+
ExpoIapLog.payload("getAvailableItemsAndroid", null)
|
|
153
202
|
scope.launch {
|
|
154
203
|
try {
|
|
155
204
|
val purchases = openIap.getAvailablePurchases(null)
|
|
156
|
-
|
|
205
|
+
val payload = purchases.map { it.toJson() }
|
|
206
|
+
ExpoIapLog.result("getAvailableItemsAndroid", payload)
|
|
207
|
+
promise.resolve(payload)
|
|
157
208
|
} catch (e: Exception) {
|
|
209
|
+
ExpoIapLog.failure("getAvailableItemsAndroid", e)
|
|
158
210
|
promise.reject(OpenIapError.ServiceUnavailable.CODE, e.message, null)
|
|
159
211
|
}
|
|
160
212
|
}
|
|
@@ -164,11 +216,22 @@ class ExpoIapModule : Module() {
|
|
|
164
216
|
AsyncFunction("deepLinkToSubscriptionsAndroid") { params: Map<String, Any?>, promise: Promise ->
|
|
165
217
|
val sku = (params["sku"] ?: params["skuAndroid"]) as? String
|
|
166
218
|
val packageName = (params["packageName"] ?: params["packageNameAndroid"]) as? String
|
|
219
|
+
ExpoIapLog.payload(
|
|
220
|
+
"deepLinkToSubscriptionsAndroid",
|
|
221
|
+
mapOf("sku" to sku, "packageName" to packageName),
|
|
222
|
+
)
|
|
167
223
|
scope.launch {
|
|
168
224
|
try {
|
|
169
|
-
openIap.deepLinkToSubscriptions(
|
|
225
|
+
openIap.deepLinkToSubscriptions(
|
|
226
|
+
DeepLinkOptions(
|
|
227
|
+
packageNameAndroid = packageName,
|
|
228
|
+
skuAndroid = sku,
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
ExpoIapLog.result("deepLinkToSubscriptionsAndroid", true)
|
|
170
232
|
promise.resolve(null)
|
|
171
233
|
} catch (e: Exception) {
|
|
234
|
+
ExpoIapLog.failure("deepLinkToSubscriptionsAndroid", e)
|
|
172
235
|
promise.reject(OpenIapError.ServiceUnavailable.CODE, e.message, null)
|
|
173
236
|
}
|
|
174
237
|
}
|
|
@@ -176,18 +239,22 @@ class ExpoIapModule : Module() {
|
|
|
176
239
|
|
|
177
240
|
// Get Google Play storefront country code (Android)
|
|
178
241
|
AsyncFunction("getStorefrontAndroid") { promise: Promise ->
|
|
242
|
+
ExpoIapLog.payload("getStorefrontAndroid", null)
|
|
179
243
|
scope.launch {
|
|
180
244
|
try {
|
|
181
245
|
val code = openIap.getStorefront()
|
|
246
|
+
ExpoIapLog.result("getStorefrontAndroid", code)
|
|
182
247
|
promise.resolve(code)
|
|
183
248
|
} catch (e: Exception) {
|
|
249
|
+
ExpoIapLog.failure("getStorefrontAndroid", e)
|
|
184
250
|
promise.reject(OpenIapError.ServiceUnavailable.CODE, e.message, e)
|
|
185
251
|
}
|
|
186
252
|
}
|
|
187
253
|
}
|
|
188
254
|
|
|
189
255
|
AsyncFunction("requestPurchase") { params: Map<String, Any?>, promise: Promise ->
|
|
190
|
-
|
|
256
|
+
ExpoIapLog.payload("requestPurchaseAndroid", params)
|
|
257
|
+
val type = params["type"] as? String
|
|
191
258
|
val skus: List<String> =
|
|
192
259
|
(params["skus"] as? List<*>)?.filterIsInstance<String>()
|
|
193
260
|
?: (params["skuArr"] as? List<*>)?.filterIsInstance<String>()
|
|
@@ -199,7 +266,7 @@ class ExpoIapModule : Module() {
|
|
|
199
266
|
val isOfferPersonalized = params["isOfferPersonalized"] as? Boolean ?: false
|
|
200
267
|
val offerTokenArr =
|
|
201
268
|
(params["offerTokenArr"] as? List<*>)?.filterIsInstance<String>() ?: emptyList()
|
|
202
|
-
val
|
|
269
|
+
val explicitSubscriptionOffers =
|
|
203
270
|
(params["subscriptionOffers"] as? List<*>)?.mapNotNull { rawOffer ->
|
|
204
271
|
val offerMap = rawOffer as? Map<*, *> ?: return@mapNotNull null
|
|
205
272
|
val sku = offerMap["sku"] as? String
|
|
@@ -207,67 +274,123 @@ class ExpoIapModule : Module() {
|
|
|
207
274
|
if (sku.isNullOrEmpty() || offerToken.isNullOrEmpty()) {
|
|
208
275
|
null
|
|
209
276
|
} else {
|
|
210
|
-
|
|
277
|
+
AndroidSubscriptionOfferInput(offerToken = offerToken, sku = sku)
|
|
211
278
|
}
|
|
212
279
|
} ?: emptyList()
|
|
280
|
+
val purchaseToken =
|
|
281
|
+
(params["purchaseTokenAndroid"] ?: params["purchaseToken"]) as? String
|
|
282
|
+
val replacementMode =
|
|
283
|
+
(params["replacementModeAndroid"] ?: params["replacementMode"]) as? Number
|
|
213
284
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (token.isNotEmpty()) {
|
|
226
|
-
RequestSubscriptionAndroidProps.SubscriptionOffer(
|
|
227
|
-
sku = sku,
|
|
228
|
-
offerToken = token,
|
|
229
|
-
)
|
|
230
|
-
} else {
|
|
231
|
-
null
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
else -> emptyList()
|
|
235
|
-
}
|
|
285
|
+
val productType =
|
|
286
|
+
when (parseProductQueryType(type)) {
|
|
287
|
+
ProductQueryType.Subs -> ProductQueryType.Subs
|
|
288
|
+
else -> ProductQueryType.InApp
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
val fallbackOffers =
|
|
292
|
+
if (explicitSubscriptionOffers.isEmpty() && offerTokenArr.isNotEmpty()) {
|
|
293
|
+
skus.zip(offerTokenArr).mapNotNull { (sku, token) ->
|
|
294
|
+
if (token.isNotEmpty()) {
|
|
295
|
+
AndroidSubscriptionOfferInput(offerToken = token, sku = sku)
|
|
236
296
|
} else {
|
|
237
|
-
|
|
297
|
+
null
|
|
238
298
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
emptyList()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
val subscriptionOffers =
|
|
305
|
+
(explicitSubscriptionOffers.ifEmpty { fallbackOffers })
|
|
306
|
+
.takeIf { it.isNotEmpty() }
|
|
307
|
+
|
|
308
|
+
val requestProps =
|
|
309
|
+
when (productType) {
|
|
310
|
+
ProductQueryType.Subs -> {
|
|
311
|
+
val android =
|
|
312
|
+
RequestSubscriptionAndroidProps(
|
|
313
|
+
isOfferPersonalized = isOfferPersonalized,
|
|
243
314
|
obfuscatedAccountIdAndroid = obfuscatedAccountId,
|
|
244
315
|
obfuscatedProfileIdAndroid = obfuscatedProfileId,
|
|
245
|
-
|
|
316
|
+
purchaseTokenAndroid = purchaseToken,
|
|
317
|
+
replacementModeAndroid = replacementMode?.toInt(),
|
|
318
|
+
skus = skus,
|
|
246
319
|
subscriptionOffers = subscriptionOffers,
|
|
247
|
-
)
|
|
248
|
-
|
|
320
|
+
)
|
|
321
|
+
RequestPurchaseProps(
|
|
322
|
+
request =
|
|
323
|
+
RequestPurchaseProps.Request.Subscription(
|
|
324
|
+
RequestSubscriptionPropsByPlatforms(android = android),
|
|
325
|
+
),
|
|
326
|
+
type = ProductQueryType.Subs,
|
|
249
327
|
)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
else -> {
|
|
331
|
+
val android =
|
|
332
|
+
RequestPurchaseAndroidProps(
|
|
333
|
+
isOfferPersonalized = isOfferPersonalized,
|
|
334
|
+
obfuscatedAccountIdAndroid = obfuscatedAccountId,
|
|
335
|
+
obfuscatedProfileIdAndroid = obfuscatedProfileId,
|
|
336
|
+
skus = skus,
|
|
337
|
+
)
|
|
338
|
+
RequestPurchaseProps(
|
|
339
|
+
request =
|
|
340
|
+
RequestPurchaseProps.Request.Purchase(
|
|
341
|
+
RequestPurchasePropsByPlatforms(android = android),
|
|
342
|
+
),
|
|
343
|
+
type = ProductQueryType.InApp,
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
PromiseUtils.addPromiseForKey(PromiseUtils.PROMISE_BUY_ITEM, promise)
|
|
349
|
+
scope.launch {
|
|
350
|
+
try {
|
|
351
|
+
openIap.setActivity(currentActivity)
|
|
352
|
+
val result = openIap.requestPurchase(requestProps)
|
|
353
|
+
val purchases =
|
|
354
|
+
when (result) {
|
|
355
|
+
is RequestPurchaseResultPurchases -> result.value.orEmpty()
|
|
356
|
+
is RequestPurchaseResultPurchase -> result.value?.let(::listOf).orEmpty()
|
|
357
|
+
else -> emptyList()
|
|
358
|
+
}
|
|
359
|
+
ExpoIapLog.result(
|
|
360
|
+
"requestPurchaseAndroid",
|
|
361
|
+
purchases.map { it.toJson() },
|
|
362
|
+
)
|
|
363
|
+
purchases.forEach { purchase ->
|
|
364
|
+
runCatching {
|
|
365
|
+
emitOrQueue(EVENT_PURCHASE_UPDATED, purchase.toJson())
|
|
366
|
+
}.onFailure { ex ->
|
|
367
|
+
Log.e(
|
|
368
|
+
TAG,
|
|
369
|
+
"Failed to send PURCHASE_UPDATED event (requestPurchase)",
|
|
370
|
+
ex,
|
|
371
|
+
)
|
|
255
372
|
}
|
|
256
373
|
}
|
|
257
|
-
PromiseUtils.resolvePromisesForKey(
|
|
374
|
+
PromiseUtils.resolvePromisesForKey(
|
|
375
|
+
PromiseUtils.PROMISE_BUY_ITEM,
|
|
376
|
+
purchases.map { it.toJson() },
|
|
377
|
+
)
|
|
258
378
|
} catch (e: Exception) {
|
|
379
|
+
ExpoIapLog.failure("requestPurchaseAndroid", e)
|
|
259
380
|
val errorMap =
|
|
260
381
|
mapOf(
|
|
261
382
|
"code" to OpenIapError.PurchaseFailed.CODE,
|
|
262
383
|
"message" to (e.message ?: "Purchase failed"),
|
|
263
384
|
"platform" to "android",
|
|
264
385
|
)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
386
|
+
runCatching { emitOrQueue(EVENT_PURCHASE_ERROR, errorMap) }
|
|
387
|
+
.onFailure { ex ->
|
|
388
|
+
Log.e(
|
|
389
|
+
TAG,
|
|
390
|
+
"Failed to send PURCHASE_ERROR event (requestPurchase)",
|
|
391
|
+
ex,
|
|
392
|
+
)
|
|
393
|
+
}
|
|
271
394
|
PromiseUtils.rejectPromisesForKey(
|
|
272
395
|
PromiseUtils.PROMISE_BUY_ITEM,
|
|
273
396
|
OpenIapError.PurchaseFailed.CODE,
|
|
@@ -279,11 +402,15 @@ class ExpoIapModule : Module() {
|
|
|
279
402
|
}
|
|
280
403
|
|
|
281
404
|
AsyncFunction("acknowledgePurchaseAndroid") { token: String, promise: Promise ->
|
|
405
|
+
ExpoIapLog.payload("acknowledgePurchaseAndroid", mapOf("token" to token))
|
|
282
406
|
scope.launch {
|
|
283
407
|
try {
|
|
284
408
|
openIap.acknowledgePurchaseAndroid(token)
|
|
285
|
-
|
|
409
|
+
val response = mapOf("responseCode" to 0)
|
|
410
|
+
ExpoIapLog.result("acknowledgePurchaseAndroid", response)
|
|
411
|
+
promise.resolve(response)
|
|
286
412
|
} catch (e: Exception) {
|
|
413
|
+
ExpoIapLog.failure("acknowledgePurchaseAndroid", e)
|
|
287
414
|
promise.reject(OpenIapError.ServiceUnavailable.CODE, e.message, null)
|
|
288
415
|
}
|
|
289
416
|
}
|
|
@@ -291,11 +418,15 @@ class ExpoIapModule : Module() {
|
|
|
291
418
|
|
|
292
419
|
// New name: consumePurchaseAndroid
|
|
293
420
|
AsyncFunction("consumePurchaseAndroid") { token: String, promise: Promise ->
|
|
421
|
+
ExpoIapLog.payload("consumePurchaseAndroid", mapOf("token" to token))
|
|
294
422
|
scope.launch {
|
|
295
423
|
try {
|
|
296
424
|
openIap.consumePurchaseAndroid(token)
|
|
297
|
-
|
|
425
|
+
val response = mapOf("responseCode" to 0, "purchaseToken" to token)
|
|
426
|
+
ExpoIapLog.result("consumePurchaseAndroid", response)
|
|
427
|
+
promise.resolve(response)
|
|
298
428
|
} catch (e: Exception) {
|
|
429
|
+
ExpoIapLog.failure("consumePurchaseAndroid", e)
|
|
299
430
|
promise.reject(OpenIapError.ServiceUnavailable.CODE, e.message, null)
|
|
300
431
|
}
|
|
301
432
|
}
|