expo-iap 4.4.0-rc.7 → 4.4.1
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/CHANGELOG.md +4 -1
- package/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/android/build.gradle +2 -0
- package/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt +189 -115
- package/android/src/main/java/expo/modules/iap/ExpoIapLog.kt +22 -3
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +224 -34
- package/build/index.d.ts +22 -20
- package/build/index.d.ts.map +1 -1
- package/build/index.js +118 -30
- package/build/index.js.map +1 -1
- package/build/index.kepler.d.ts +3 -0
- package/build/index.kepler.d.ts.map +1 -1
- package/build/index.kepler.js +3 -0
- package/build/index.kepler.js.map +1 -1
- package/build/modules/android.d.ts +35 -19
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +57 -6
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +8 -8
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +29 -2
- package/build/modules/ios.js.map +1 -1
- package/build/types.d.ts +279 -59
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/useIAP.d.ts +23 -3
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +43 -7
- package/build/useIAP.js.map +1 -1
- package/build/utils/errorMapping.d.ts +27 -3
- package/build/utils/errorMapping.d.ts.map +1 -1
- package/build/utils/errorMapping.js +119 -43
- package/build/utils/errorMapping.js.map +1 -1
- package/build/vega-adapter.d.ts.map +1 -1
- package/build/vega-adapter.js +37 -4
- package/build/vega-adapter.js.map +1 -1
- package/ios/ExpoIapHelper.swift +22 -8
- package/ios/ExpoIapLog.swift +13 -1
- package/ios/ExpoIapModule.swift +26 -7
- package/ios/onside/OnsideIapModule.swift +9 -1
- package/openiap-versions.json +3 -3
- package/package.json +1 -1
- package/plugin/build/withIAP.js +16 -9
- package/plugin/src/withIAP.ts +20 -12
- package/src/index.kepler.ts +9 -0
- package/src/index.ts +182 -55
- package/src/modules/android.ts +105 -7
- package/src/modules/ios.ts +57 -25
- package/src/types.ts +294 -59
- package/src/useIAP.ts +102 -17
- package/src/utils/errorMapping.ts +177 -71
- package/src/vega-adapter.ts +77 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenIAP keeps detailed release history centralized to avoid package-local drift.
|
|
4
|
+
|
|
5
|
+
- Consolidated release notes: [openiap.dev/docs/updates/releases](https://openiap.dev/docs/updates/releases)
|
|
6
|
+
- Expo package releases: [hyodotdev/openiap/releases?q=expo-iap](https://github.com/hyodotdev/openiap/releases?q=expo-iap&expanded=true)
|
package/CLAUDE.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
Before committing any changes:
|
|
40
40
|
|
|
41
41
|
1. Run `bun run lint` to ensure code quality
|
|
42
|
-
2. Run `bun run
|
|
42
|
+
2. Run `bun run lint:tsc` to verify TypeScript types
|
|
43
43
|
3. Run `bun run test` to verify all tests pass (Note: Use `bun run test`, not `bun test`)
|
|
44
44
|
4. **IMPORTANT**: Run tests in the example directory as well:
|
|
45
45
|
- `cd example && bun run test`
|
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Expo IAP conforms to the **[OpenIAP specification](https://openiap.dev)** — an
|
|
|
85
85
|
|
|
86
86
|
- **Shared specification** — Common types, error codes, and purchase flows across all platforms
|
|
87
87
|
- **Generated type-safe bindings** — Swift, Kotlin, Dart, and GDScript from a single GraphQL schema
|
|
88
|
-
- **Platform implementations** — [openiap-apple](https://github.com/hyodotdev/openiap/tree/main/packages/apple) (StoreKit 2) and [openiap-google](https://github.com/hyodotdev/openiap/tree/main/packages/google) (Play Billing
|
|
88
|
+
- **Platform implementations** — [openiap-apple](https://github.com/hyodotdev/openiap/tree/main/packages/apple) (StoreKit 2) and [openiap-google](https://github.com/hyodotdev/openiap/tree/main/packages/google) (Play Billing 9.1.0)
|
|
89
89
|
- **Verification profiles** — Standardized receipt validation and purchase verification patterns
|
|
90
90
|
|
|
91
91
|
Other libraries built on OpenIAP: [react-native-iap](https://github.com/hyodotdev/openiap/tree/main/libraries/react-native-iap) · [flutter_inapp_purchase](https://github.com/hyodotdev/openiap/tree/main/libraries/flutter_inapp_purchase) · [kmp-iap](https://github.com/hyodotdev/openiap/tree/main/libraries/kmp-iap) · [godot-iap](https://github.com/hyodotdev/openiap/tree/main/libraries/godot-iap)
|
package/android/build.gradle
CHANGED
|
@@ -103,6 +103,8 @@ kotlin {
|
|
|
103
103
|
|
|
104
104
|
dependencies {
|
|
105
105
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
|
|
106
|
+
testImplementation "junit:junit:4.13.2"
|
|
107
|
+
testImplementation "org.json:json:20180813"
|
|
106
108
|
|
|
107
109
|
// Use OpenIAP Google module only; avoid direct BillingClient dependency
|
|
108
110
|
if (findProject(":openiap-google") != null) {
|
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
package expo.modules.iap
|
|
2
2
|
|
|
3
3
|
import dev.hyo.openiap.AndroidSubscriptionOfferInput
|
|
4
|
+
import dev.hyo.openiap.DeveloperBillingOptionParamsAndroid
|
|
4
5
|
import dev.hyo.openiap.OpenIapError
|
|
5
6
|
import dev.hyo.openiap.OpenIapModule
|
|
6
7
|
import dev.hyo.openiap.ProductQueryType
|
|
7
8
|
import dev.hyo.openiap.SubscriptionProductReplacementParamsAndroid
|
|
8
9
|
import dev.hyo.openiap.SubscriptionReplacementModeAndroid
|
|
10
|
+
import dev.hyo.openiap.listener.OpenIapDeveloperProvidedBillingListener
|
|
11
|
+
import dev.hyo.openiap.listener.OpenIapPurchaseErrorListener
|
|
12
|
+
import dev.hyo.openiap.listener.OpenIapPurchaseUpdateListener
|
|
13
|
+
import dev.hyo.openiap.listener.OpenIapSubscriptionBillingIssueListener
|
|
14
|
+
import dev.hyo.openiap.listener.OpenIapUserChoiceBillingListener
|
|
9
15
|
import expo.modules.kotlin.Promise
|
|
10
16
|
import expo.modules.kotlin.modules.Module
|
|
11
17
|
import kotlinx.coroutines.CoroutineScope
|
|
12
18
|
import kotlinx.coroutines.launch
|
|
19
|
+
import org.json.JSONObject
|
|
13
20
|
import java.util.Locale
|
|
14
21
|
import java.util.concurrent.ConcurrentLinkedQueue
|
|
15
22
|
|
|
16
23
|
object ExpoIapHelper {
|
|
17
24
|
private const val MAX_BUFFERED_EVENTS = 200
|
|
25
|
+
internal const val ERROR_ENVELOPE_PREFIX = "OPENIAP_ERROR_JSON:"
|
|
26
|
+
|
|
27
|
+
data class ListenerHandles(
|
|
28
|
+
val purchaseUpdate: OpenIapPurchaseUpdateListener,
|
|
29
|
+
val purchaseError: OpenIapPurchaseErrorListener,
|
|
30
|
+
val userChoiceBilling: OpenIapUserChoiceBillingListener,
|
|
31
|
+
val developerProvidedBilling: OpenIapDeveloperProvidedBillingListener,
|
|
32
|
+
val subscriptionBillingIssue: OpenIapSubscriptionBillingIssueListener,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
internal fun serializeOpenIapError(error: OpenIapError): Map<String, Any?> =
|
|
36
|
+
error.toJSON() +
|
|
37
|
+
when (error) {
|
|
38
|
+
is OpenIapError.ProductNotFound -> mapOf("productId" to error.productId)
|
|
39
|
+
is OpenIapError.SkuNotFound -> mapOf("productId" to error.sku)
|
|
40
|
+
else -> emptyMap()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
internal fun serializeErrorEnvelope(payload: Map<String, Any?>): String = ERROR_ENVELOPE_PREFIX + JSONObject(payload).toString()
|
|
18
44
|
|
|
19
45
|
fun emitOrQueue(
|
|
20
46
|
module: Module,
|
|
@@ -56,29 +82,31 @@ object ExpoIapHelper {
|
|
|
56
82
|
// Support nested request.google / request.android structure
|
|
57
83
|
// If the params contain a "request" key with nested platform-specific data,
|
|
58
84
|
// extract and flatten it before parsing.
|
|
59
|
-
val effective: Map<String, Any?> =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
val effective: Map<String, Any?> =
|
|
86
|
+
run {
|
|
87
|
+
val request = params["request"] as? Map<*, *>
|
|
88
|
+
if (request != null) {
|
|
89
|
+
val nested =
|
|
90
|
+
(request["google"] as? Map<*, *>)
|
|
91
|
+
?: (request["android"] as? Map<*, *>)
|
|
92
|
+
if (nested != null) {
|
|
93
|
+
val flat = mutableMapOf<String, Any?>()
|
|
94
|
+
// Carry over top-level fields like type, useAlternativeBilling
|
|
95
|
+
for ((k, v) in params) {
|
|
96
|
+
if (k != "request") flat[k] = v
|
|
97
|
+
}
|
|
98
|
+
// Overlay platform-specific fields
|
|
99
|
+
for ((k, v) in nested) {
|
|
100
|
+
if (k is String) flat[k] = v
|
|
101
|
+
}
|
|
102
|
+
flat
|
|
103
|
+
} else {
|
|
104
|
+
params
|
|
73
105
|
}
|
|
74
|
-
flat
|
|
75
106
|
} else {
|
|
76
107
|
params
|
|
77
108
|
}
|
|
78
|
-
} else {
|
|
79
|
-
params
|
|
80
109
|
}
|
|
81
|
-
}
|
|
82
110
|
|
|
83
111
|
val type = effective["type"] as? String
|
|
84
112
|
val skus: List<String> =
|
|
@@ -102,7 +130,17 @@ object ExpoIapHelper {
|
|
|
102
130
|
}
|
|
103
131
|
} ?: emptyList()
|
|
104
132
|
val purchaseToken = effective["purchaseToken"] as? String
|
|
133
|
+
val originalExternalTransactionId = effective["originalExternalTransactionId"] as? String
|
|
105
134
|
val replacementMode = effective["replacementMode"] as? Number
|
|
135
|
+
val developerBillingOption =
|
|
136
|
+
(effective["developerBillingOption"] as? Map<*, *>)?.let { optionMap ->
|
|
137
|
+
val json =
|
|
138
|
+
optionMap.entries
|
|
139
|
+
.mapNotNull { (key, value) ->
|
|
140
|
+
(key as? String)?.let { it to value }
|
|
141
|
+
}.toMap()
|
|
142
|
+
DeveloperBillingOptionParamsAndroid.fromJson(json)
|
|
143
|
+
}
|
|
106
144
|
val subscriptionProductReplacementParams =
|
|
107
145
|
(effective["subscriptionProductReplacementParams"] as? Map<*, *>)?.let { paramsMap ->
|
|
108
146
|
val oldProductId = paramsMap["oldProductId"] as? String
|
|
@@ -116,7 +154,7 @@ object ExpoIapHelper {
|
|
|
116
154
|
)
|
|
117
155
|
}
|
|
118
156
|
}
|
|
119
|
-
// offerToken for one-time purchase discounts (Android
|
|
157
|
+
// offerToken for one-time purchase discounts (Android 8.0+)
|
|
120
158
|
val offerToken = effective["offerToken"] as? String
|
|
121
159
|
|
|
122
160
|
return RequestPurchaseParams(
|
|
@@ -128,6 +166,8 @@ object ExpoIapHelper {
|
|
|
128
166
|
offerToken = offerToken,
|
|
129
167
|
offerTokenArr = offerTokenArr,
|
|
130
168
|
explicitSubscriptionOffers = explicitSubscriptionOffers,
|
|
169
|
+
developerBillingOption = developerBillingOption,
|
|
170
|
+
originalExternalTransactionId = originalExternalTransactionId,
|
|
131
171
|
purchaseToken = purchaseToken,
|
|
132
172
|
replacementMode = replacementMode,
|
|
133
173
|
subscriptionProductReplacementParams = subscriptionProductReplacementParams,
|
|
@@ -151,10 +191,12 @@ object ExpoIapHelper {
|
|
|
151
191
|
val obfuscatedAccountId: String?,
|
|
152
192
|
val obfuscatedProfileId: String?,
|
|
153
193
|
val isOfferPersonalized: Boolean,
|
|
154
|
-
/** Offer token for one-time purchase discounts (Android
|
|
194
|
+
/** Offer token for one-time purchase discounts (Android 8.0+) */
|
|
155
195
|
val offerToken: String?,
|
|
156
196
|
val offerTokenArr: List<String>,
|
|
157
197
|
val explicitSubscriptionOffers: List<AndroidSubscriptionOfferInput>,
|
|
198
|
+
val developerBillingOption: DeveloperBillingOptionParamsAndroid?,
|
|
199
|
+
val originalExternalTransactionId: String?,
|
|
158
200
|
val purchaseToken: String?,
|
|
159
201
|
val replacementMode: Number?,
|
|
160
202
|
val subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid?,
|
|
@@ -226,56 +268,43 @@ object ExpoIapHelper {
|
|
|
226
268
|
eventUserChoiceBilling: String,
|
|
227
269
|
eventDeveloperProvidedBilling: String,
|
|
228
270
|
eventSubscriptionBillingIssue: String,
|
|
229
|
-
) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
emitOrQueue(
|
|
233
|
-
module,
|
|
234
|
-
scope,
|
|
235
|
-
connectionReady,
|
|
236
|
-
pendingEvents,
|
|
237
|
-
eventPurchaseUpdated,
|
|
238
|
-
p.toJson(),
|
|
239
|
-
)
|
|
240
|
-
}.onFailure { error ->
|
|
241
|
-
ExpoIapLog.failure("buffer/send PURCHASE_UPDATED", error)
|
|
242
|
-
// Emit as purchase error so user knows something went wrong
|
|
243
|
-
val errorPayload =
|
|
244
|
-
mapOf(
|
|
245
|
-
"code" to OpenIapError.PurchaseFailed.CODE,
|
|
246
|
-
"message" to "Failed to process purchase update: ${error.message}",
|
|
247
|
-
)
|
|
271
|
+
): ListenerHandles {
|
|
272
|
+
val purchaseUpdateListener =
|
|
273
|
+
OpenIapPurchaseUpdateListener { p ->
|
|
248
274
|
runCatching {
|
|
249
275
|
emitOrQueue(
|
|
250
276
|
module,
|
|
251
277
|
scope,
|
|
252
278
|
connectionReady,
|
|
253
279
|
pendingEvents,
|
|
254
|
-
|
|
255
|
-
|
|
280
|
+
eventPurchaseUpdated,
|
|
281
|
+
p.toJson(),
|
|
256
282
|
)
|
|
257
|
-
}.onFailure {
|
|
283
|
+
}.onFailure { error ->
|
|
284
|
+
ExpoIapLog.failure("buffer/send PURCHASE_UPDATED", error)
|
|
285
|
+
// Emit as purchase error so user knows something went wrong
|
|
286
|
+
val errorPayload =
|
|
287
|
+
mapOf(
|
|
288
|
+
"code" to OpenIapError.PurchaseFailed.CODE,
|
|
289
|
+
"message" to "Failed to process purchase update: ${error.message}",
|
|
290
|
+
)
|
|
291
|
+
runCatching {
|
|
292
|
+
emitOrQueue(
|
|
293
|
+
module,
|
|
294
|
+
scope,
|
|
295
|
+
connectionReady,
|
|
296
|
+
pendingEvents,
|
|
297
|
+
eventPurchaseError,
|
|
298
|
+
errorPayload,
|
|
299
|
+
)
|
|
300
|
+
}.onFailure { ExpoIapLog.failure("send error event", it) }
|
|
301
|
+
}
|
|
258
302
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
module,
|
|
265
|
-
scope,
|
|
266
|
-
connectionReady,
|
|
267
|
-
pendingEvents,
|
|
268
|
-
eventPurchaseError,
|
|
269
|
-
errorJson,
|
|
270
|
-
)
|
|
271
|
-
}.onFailure { error ->
|
|
272
|
-
ExpoIapLog.failure("buffer/send PURCHASE_ERROR", error)
|
|
273
|
-
// Critical: if we can't emit the original error, at least try to emit a generic one
|
|
274
|
-
val fallbackPayload =
|
|
275
|
-
mapOf(
|
|
276
|
-
"code" to OpenIapError.UnknownError.CODE,
|
|
277
|
-
"message" to "Failed to emit purchase error: ${error.message}",
|
|
278
|
-
)
|
|
303
|
+
openIap.addPurchaseUpdateListener(purchaseUpdateListener)
|
|
304
|
+
|
|
305
|
+
val purchaseErrorListener =
|
|
306
|
+
OpenIapPurchaseErrorListener { e ->
|
|
307
|
+
val errorJson = serializeOpenIapError(e)
|
|
279
308
|
runCatching {
|
|
280
309
|
emitOrQueue(
|
|
281
310
|
module,
|
|
@@ -283,63 +312,108 @@ object ExpoIapHelper {
|
|
|
283
312
|
connectionReady,
|
|
284
313
|
pendingEvents,
|
|
285
314
|
eventPurchaseError,
|
|
286
|
-
|
|
315
|
+
errorJson,
|
|
287
316
|
)
|
|
288
|
-
}.onFailure {
|
|
317
|
+
}.onFailure { error ->
|
|
318
|
+
ExpoIapLog.failure("buffer/send PURCHASE_ERROR", error)
|
|
319
|
+
// Critical: if we can't emit the original error, at least try to emit a generic one
|
|
320
|
+
val fallbackPayload =
|
|
321
|
+
mapOf(
|
|
322
|
+
"code" to OpenIapError.UnknownError.CODE,
|
|
323
|
+
"message" to "Failed to emit purchase error: ${error.message}",
|
|
324
|
+
)
|
|
325
|
+
runCatching {
|
|
326
|
+
emitOrQueue(
|
|
327
|
+
module,
|
|
328
|
+
scope,
|
|
329
|
+
connectionReady,
|
|
330
|
+
pendingEvents,
|
|
331
|
+
eventPurchaseError,
|
|
332
|
+
fallbackPayload,
|
|
333
|
+
)
|
|
334
|
+
}.onFailure { ExpoIapLog.failure("send fallback error event", it) }
|
|
335
|
+
}
|
|
336
|
+
// Also reject any pending purchase promises to match iOS behavior
|
|
337
|
+
val errorCode = errorJson["code"] as? String ?: OpenIapError.PurchaseFailed.CODE
|
|
338
|
+
rejectPurchasePromises(
|
|
339
|
+
errorCode,
|
|
340
|
+
serializeErrorEnvelope(errorJson),
|
|
341
|
+
null,
|
|
342
|
+
)
|
|
289
343
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
// Developer Provided Billing listener for External Payments (8.3.0+)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
344
|
+
openIap.addPurchaseErrorListener(purchaseErrorListener)
|
|
345
|
+
|
|
346
|
+
val userChoiceBillingListener =
|
|
347
|
+
OpenIapUserChoiceBillingListener { details ->
|
|
348
|
+
safeEmitEvent(
|
|
349
|
+
module,
|
|
350
|
+
scope,
|
|
351
|
+
connectionReady,
|
|
352
|
+
pendingEvents,
|
|
353
|
+
eventUserChoiceBilling,
|
|
354
|
+
details.toJson(),
|
|
355
|
+
eventPurchaseError,
|
|
356
|
+
"alternative-billing-not-available",
|
|
357
|
+
"Failed to process user choice billing",
|
|
358
|
+
"USER_CHOICE_BILLING",
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
openIap.addUserChoiceBillingListener(userChoiceBillingListener)
|
|
362
|
+
|
|
363
|
+
// Developer Provided Billing listener for External Payments (8.3.0+) and Billing Choice (9.1.0+)
|
|
364
|
+
val developerProvidedBillingListener =
|
|
365
|
+
OpenIapDeveloperProvidedBillingListener { details ->
|
|
366
|
+
safeEmitEvent(
|
|
367
|
+
module,
|
|
368
|
+
scope,
|
|
369
|
+
connectionReady,
|
|
370
|
+
pendingEvents,
|
|
371
|
+
eventDeveloperProvidedBilling,
|
|
372
|
+
details.toJson(),
|
|
373
|
+
eventPurchaseError,
|
|
374
|
+
"developer-billing-error",
|
|
375
|
+
"Failed to process developer provided billing",
|
|
376
|
+
"DEVELOPER_PROVIDED_BILLING",
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
openIap.addDeveloperProvidedBillingListener(developerProvidedBillingListener)
|
|
380
|
+
|
|
324
381
|
// Subscription billing-issue listener (Play Billing 8.1+ isSuspended; no-op on Horizon)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
382
|
+
val subscriptionBillingIssueListener =
|
|
383
|
+
OpenIapSubscriptionBillingIssueListener { purchase ->
|
|
384
|
+
safeEmitEvent(
|
|
385
|
+
module,
|
|
386
|
+
scope,
|
|
387
|
+
connectionReady,
|
|
388
|
+
pendingEvents,
|
|
389
|
+
eventSubscriptionBillingIssue,
|
|
390
|
+
purchase.toJson(),
|
|
391
|
+
eventPurchaseError,
|
|
392
|
+
"subscription-billing-issue-error",
|
|
393
|
+
"Failed to process subscription billing issue",
|
|
394
|
+
"SUBSCRIPTION_BILLING_ISSUE",
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
openIap.addSubscriptionBillingIssueListener(subscriptionBillingIssueListener)
|
|
398
|
+
|
|
399
|
+
return ListenerHandles(
|
|
400
|
+
purchaseUpdate = purchaseUpdateListener,
|
|
401
|
+
purchaseError = purchaseErrorListener,
|
|
402
|
+
userChoiceBilling = userChoiceBillingListener,
|
|
403
|
+
developerProvidedBilling = developerProvidedBillingListener,
|
|
404
|
+
subscriptionBillingIssue = subscriptionBillingIssueListener,
|
|
405
|
+
)
|
|
339
406
|
}
|
|
340
407
|
|
|
341
|
-
fun cleanupListeners(
|
|
342
|
-
|
|
343
|
-
|
|
408
|
+
fun cleanupListeners(
|
|
409
|
+
openIap: OpenIapModule,
|
|
410
|
+
handles: ListenerHandles?,
|
|
411
|
+
) {
|
|
412
|
+
if (handles == null) return
|
|
413
|
+
openIap.removePurchaseUpdateListener(handles.purchaseUpdate)
|
|
414
|
+
openIap.removePurchaseErrorListener(handles.purchaseError)
|
|
415
|
+
openIap.removeUserChoiceBillingListener(handles.userChoiceBilling)
|
|
416
|
+
openIap.removeDeveloperProvidedBillingListener(handles.developerProvidedBilling)
|
|
417
|
+
openIap.removeSubscriptionBillingIssueListener(handles.subscriptionBillingIssue)
|
|
344
418
|
}
|
|
345
419
|
}
|
|
@@ -12,7 +12,9 @@ internal object ExpoIapLog {
|
|
|
12
12
|
"apikey",
|
|
13
13
|
"secret",
|
|
14
14
|
"jws",
|
|
15
|
-
"
|
|
15
|
+
"receipt",
|
|
16
|
+
"dataandroid",
|
|
17
|
+
"signatureandroid",
|
|
16
18
|
"userid",
|
|
17
19
|
"password",
|
|
18
20
|
"bearer"
|
|
@@ -54,7 +56,7 @@ internal object ExpoIapLog {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
internal fun stringify(value: Any?): String {
|
|
58
60
|
val sanitized = sanitize(value) ?: return "null"
|
|
59
61
|
return when (sanitized) {
|
|
60
62
|
is String -> sanitized
|
|
@@ -66,9 +68,14 @@ internal object ExpoIapLog {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
private fun sanitize(value: Any?): Any? {
|
|
69
|
-
if (value == null) return null
|
|
71
|
+
if (value == null || value === JSONObject.NULL) return null
|
|
70
72
|
|
|
71
73
|
return when (value) {
|
|
74
|
+
is String -> sanitizeJsonString(value)
|
|
75
|
+
is JSONObject -> sanitizeMap(
|
|
76
|
+
value.keys().asSequence().associateWith(value::opt),
|
|
77
|
+
)
|
|
78
|
+
is JSONArray -> (0 until value.length()).mapNotNull { sanitize(value.opt(it)) }
|
|
72
79
|
is Map<*, *> -> sanitizeMap(value)
|
|
73
80
|
is List<*> -> value.mapNotNull { sanitize(it) }
|
|
74
81
|
is Array<*> -> value.mapNotNull { sanitize(it) }
|
|
@@ -76,6 +83,18 @@ internal object ExpoIapLog {
|
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
private fun sanitizeJsonString(value: String): Any {
|
|
87
|
+
val trimmed = value.trim()
|
|
88
|
+
val json = runCatching {
|
|
89
|
+
when (trimmed.firstOrNull()) {
|
|
90
|
+
'{' -> JSONObject(trimmed)
|
|
91
|
+
'[' -> JSONArray(trimmed)
|
|
92
|
+
else -> null
|
|
93
|
+
}
|
|
94
|
+
}.getOrNull() ?: return value
|
|
95
|
+
return sanitize(json) ?: value
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
private fun sanitizeMap(source: Map<*, *>): Map<String, Any?> {
|
|
80
99
|
fun isSensitiveKey(key: String): Boolean {
|
|
81
100
|
val normalized = key.lowercase(Locale.ROOT).filter { it.isLetterOrDigit() }
|