expo-iap 2.9.6 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +24 -0
- package/CHANGELOG.md +43 -0
- package/README.md +1 -1
- package/android/build.gradle +7 -2
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +195 -668
- package/android/src/main/java/expo/modules/iap/PromiseUtils.kt +85 -0
- package/build/ExpoIap.types.d.ts +0 -6
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js.map +1 -1
- package/build/helpers/subscription.d.ts.map +1 -1
- package/build/helpers/subscription.js +14 -3
- package/build/helpers/subscription.js.map +1 -1
- package/build/index.d.ts +6 -73
- package/build/index.d.ts.map +1 -1
- package/build/index.js +21 -154
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +2 -2
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +11 -1
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +0 -60
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +2 -121
- package/build/modules/ios.js.map +1 -1
- package/build/types/ExpoIapAndroid.types.d.ts +0 -8
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
- package/build/types/ExpoIapAndroid.types.js +0 -1
- package/build/types/ExpoIapAndroid.types.js.map +1 -1
- package/build/types/ExpoIapIOS.types.d.ts +0 -5
- package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
- package/build/types/ExpoIapIOS.types.js.map +1 -1
- package/build/useIAP.d.ts +0 -18
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +1 -18
- package/build/useIAP.js.map +1 -1
- package/bun.lock +340 -137
- package/codecov.yml +17 -21
- package/ios/ExpoIapModule.swift +50 -23
- package/jest.config.js +5 -9
- package/package.json +5 -3
- package/plugin/build/withIAP.d.ts +4 -1
- package/plugin/build/withIAP.js +38 -24
- package/plugin/build/withLocalOpenIAP.d.ts +6 -2
- package/plugin/build/withLocalOpenIAP.js +175 -20
- package/plugin/src/withIAP.ts +66 -30
- package/plugin/src/withLocalOpenIAP.ts +228 -24
- package/src/ExpoIap.types.ts +0 -8
- package/src/helpers/subscription.ts +14 -3
- package/src/index.ts +22 -230
- package/src/modules/android.ts +16 -6
- package/src/modules/ios.ts +2 -168
- package/src/types/ExpoIapAndroid.types.ts +0 -11
- package/src/types/ExpoIapIOS.types.ts +0 -5
- package/src/useIAP.ts +3 -55
- package/android/src/main/java/expo/modules/iap/PlayUtils.kt +0 -178
- package/android/src/main/java/expo/modules/iap/Types.kt +0 -98
|
@@ -2,745 +2,272 @@ package expo.modules.iap
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.util.Log
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams
|
|
11
|
-
import com.android.billingclient.api.BillingConfigResponseListener
|
|
12
|
-
import com.android.billingclient.api.BillingResult
|
|
13
|
-
import com.android.billingclient.api.ConsumeParams
|
|
14
|
-
import com.android.billingclient.api.GetBillingConfigParams
|
|
15
|
-
import com.android.billingclient.api.PendingPurchasesParams
|
|
16
|
-
import com.android.billingclient.api.ProductDetails
|
|
17
|
-
import com.android.billingclient.api.ProductDetailsResult
|
|
18
|
-
import com.android.billingclient.api.Purchase
|
|
19
|
-
import com.android.billingclient.api.PurchasesUpdatedListener
|
|
20
|
-
import com.android.billingclient.api.QueryProductDetailsParams
|
|
21
|
-
import com.android.billingclient.api.QueryProductDetailsResult
|
|
22
|
-
import com.android.billingclient.api.QueryPurchasesParams
|
|
23
|
-
import com.google.android.gms.common.ConnectionResult
|
|
24
|
-
import com.google.android.gms.common.GoogleApiAvailability
|
|
5
|
+
import dev.hyo.openiap.OpenIapError
|
|
6
|
+
import dev.hyo.openiap.OpenIapModule
|
|
7
|
+
import dev.hyo.openiap.models.DeepLinkOptions
|
|
8
|
+
import dev.hyo.openiap.models.ProductRequest
|
|
9
|
+
import dev.hyo.openiap.models.RequestPurchaseAndroidProps
|
|
25
10
|
import expo.modules.kotlin.Promise
|
|
26
11
|
import expo.modules.kotlin.exception.Exceptions
|
|
27
12
|
import expo.modules.kotlin.modules.Module
|
|
28
13
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
14
|
+
import kotlinx.coroutines.CoroutineScope
|
|
15
|
+
import kotlinx.coroutines.Dispatchers
|
|
16
|
+
import kotlinx.coroutines.Job
|
|
17
|
+
import kotlinx.coroutines.launch
|
|
18
|
+
import kotlinx.coroutines.sync.Mutex
|
|
19
|
+
import kotlinx.coroutines.sync.withLock
|
|
20
|
+
import java.util.concurrent.ConcurrentLinkedQueue
|
|
21
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
22
|
+
|
|
23
|
+
class ExpoIapModule : Module() {
|
|
33
24
|
companion object {
|
|
34
25
|
const val TAG = "ExpoIapModule"
|
|
26
|
+
private const val EVENT_PURCHASE_UPDATED = "purchase-updated"
|
|
27
|
+
private const val EVENT_PURCHASE_ERROR = "purchase-error"
|
|
28
|
+
private const val MAX_BUFFERED_EVENTS = 200
|
|
35
29
|
}
|
|
36
30
|
|
|
37
|
-
private
|
|
38
|
-
private val
|
|
31
|
+
private val job = Job()
|
|
32
|
+
private val scope = CoroutineScope(Dispatchers.Main + job)
|
|
39
33
|
private val context: Context
|
|
40
34
|
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
41
35
|
private val currentActivity
|
|
42
|
-
get() =
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
get() = appContext.activityProvider?.currentActivity ?: throw Exceptions.MissingActivity()
|
|
37
|
+
|
|
38
|
+
private val openIap: OpenIapModule by lazy { OpenIapModule(context) }
|
|
39
|
+
private var listenersAttached = false
|
|
40
|
+
private val pendingEvents = ConcurrentLinkedQueue<Pair<String, Map<String, Any?>>>()
|
|
41
|
+
private val connectionReady = AtomicBoolean(false)
|
|
42
|
+
private val connectionMutex = Mutex()
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
private fun emitOrQueue(
|
|
45
|
+
name: String,
|
|
46
|
+
payload: Map<String, Any?>,
|
|
49
47
|
) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
mutableMapOf<String, Any?>(
|
|
54
|
-
"responseCode" to responseCode,
|
|
55
|
-
"debugMessage" to billingResult.debugMessage,
|
|
56
|
-
)
|
|
57
|
-
// Add sub-response code if available (v8.0.0+)
|
|
58
|
-
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
|
59
|
-
try {
|
|
60
|
-
val subResponseCode = billingResult.javaClass.getMethod("getSubResponseCode").invoke(billingResult) as? Int
|
|
61
|
-
if (subResponseCode != null && subResponseCode != 0) {
|
|
62
|
-
error["subResponseCode"] = subResponseCode
|
|
63
|
-
// Check for specific sub-response codes
|
|
64
|
-
if (subResponseCode == 1) { // PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS
|
|
65
|
-
error["subResponseMessage"] = "Payment declined due to insufficient funds"
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} catch (e: Exception) {
|
|
69
|
-
// Method doesn't exist in older versions, ignore
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
val errorData = PlayUtils.getBillingResponseData(responseCode)
|
|
73
|
-
error["code"] = errorData.code
|
|
74
|
-
error["message"] = errorData.message
|
|
75
|
-
try {
|
|
76
|
-
sendEvent(OpenIapEvent.PURCHASE_ERROR, error.toMap())
|
|
77
|
-
} catch (e: Exception) {
|
|
78
|
-
Log.e(TAG, "Failed to send PURCHASE_ERROR event: ${e.message}")
|
|
79
|
-
}
|
|
80
|
-
PromiseUtils.rejectPromisesForKey(IapConstants.PROMISE_BUY_ITEM, errorData.code, errorData.message, null)
|
|
48
|
+
if (connectionReady.get()) {
|
|
49
|
+
// Ensure event emission occurs on the main dispatcher
|
|
50
|
+
scope.launch { sendEvent(name, payload) }
|
|
81
51
|
return
|
|
82
52
|
}
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
val item =
|
|
88
|
-
mutableMapOf<String, Any?>(
|
|
89
|
-
"id" to purchase.orderId,
|
|
90
|
-
"productId" to purchase.products.firstOrNull() as Any?,
|
|
91
|
-
"ids" to purchase.products,
|
|
92
|
-
"transactionId" to purchase.orderId, // @deprecated - use id instead
|
|
93
|
-
"transactionDate" to purchase.purchaseTime.toDouble(),
|
|
94
|
-
"transactionReceipt" to purchase.originalJson,
|
|
95
|
-
"purchaseTokenAndroid" to purchase.purchaseToken,
|
|
96
|
-
"purchaseToken" to purchase.purchaseToken,
|
|
97
|
-
"dataAndroid" to purchase.originalJson,
|
|
98
|
-
"signatureAndroid" to purchase.signature,
|
|
99
|
-
"autoRenewingAndroid" to purchase.isAutoRenewing,
|
|
100
|
-
"isAcknowledgedAndroid" to purchase.isAcknowledged,
|
|
101
|
-
"purchaseStateAndroid" to purchase.purchaseState,
|
|
102
|
-
"packageNameAndroid" to purchase.packageName,
|
|
103
|
-
"developerPayloadAndroid" to purchase.developerPayload,
|
|
104
|
-
"platform" to "android",
|
|
105
|
-
)
|
|
106
|
-
purchase.accountIdentifiers?.let { accountIdentifiers ->
|
|
107
|
-
item["obfuscatedAccountIdAndroid"] = accountIdentifiers.obfuscatedAccountId
|
|
108
|
-
item["obfuscatedProfileIdAndroid"] = accountIdentifiers.obfuscatedProfileId
|
|
109
|
-
}
|
|
110
|
-
promiseItems.add(item.toMap())
|
|
111
|
-
try {
|
|
112
|
-
sendEvent(OpenIapEvent.PURCHASE_UPDATED, item.toMap())
|
|
113
|
-
} catch (e: Exception) {
|
|
114
|
-
Log.e(TAG, "Failed to send PURCHASE_UPDATED event: ${e.message}")
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
PromiseUtils.resolvePromisesForKey(IapConstants.PROMISE_BUY_ITEM, promiseItems)
|
|
118
|
-
} else {
|
|
119
|
-
val result =
|
|
120
|
-
mutableMapOf<String, Any?>(
|
|
121
|
-
"platform" to "android",
|
|
122
|
-
"responseCode" to billingResult.responseCode,
|
|
123
|
-
"debugMessage" to billingResult.debugMessage,
|
|
124
|
-
"extraMessage" to
|
|
125
|
-
"The purchases are null. This is a normal behavior if you have requested DEFERRED proration. If not please report an issue.",
|
|
126
|
-
)
|
|
127
|
-
try {
|
|
128
|
-
sendEvent(OpenIapEvent.PURCHASE_UPDATED, result.toMap())
|
|
129
|
-
} catch (e: Exception) {
|
|
130
|
-
Log.e(TAG, "Failed to send PURCHASE_UPDATED event: ${e.message}")
|
|
131
|
-
}
|
|
132
|
-
PromiseUtils.resolvePromisesForKey(IapConstants.PROMISE_BUY_ITEM, result)
|
|
53
|
+
// Bound the buffer to prevent unbounded growth if init stalls
|
|
54
|
+
if (pendingEvents.size >= MAX_BUFFERED_EVENTS) {
|
|
55
|
+
pendingEvents.poll()
|
|
56
|
+
Log.w(TAG, "pendingEvents overflow; dropping oldest")
|
|
133
57
|
}
|
|
58
|
+
pendingEvents.add(name to payload)
|
|
134
59
|
}
|
|
135
60
|
|
|
61
|
+
// Mapping helpers now provided by openiap-google (toJSON helpers)
|
|
62
|
+
|
|
136
63
|
override fun definition() =
|
|
137
64
|
ModuleDefinition {
|
|
138
65
|
Name("ExpoIap")
|
|
139
66
|
|
|
140
67
|
Constants(
|
|
141
|
-
"ERROR_CODES" to
|
|
68
|
+
"ERROR_CODES" to OpenIapError.getAllErrorCodes(),
|
|
142
69
|
)
|
|
143
70
|
|
|
144
|
-
Events(
|
|
71
|
+
Events(EVENT_PURCHASE_UPDATED, EVENT_PURCHASE_ERROR)
|
|
145
72
|
|
|
146
73
|
AsyncFunction("initConnection") { promise: Promise ->
|
|
147
|
-
|
|
148
|
-
|
|
74
|
+
scope.launch {
|
|
75
|
+
connectionMutex.withLock {
|
|
76
|
+
try {
|
|
77
|
+
// Activity may be unavailable in headless/background scenarios.
|
|
78
|
+
// Attempt to set it, but do not fail init if missing.
|
|
79
|
+
runCatching { openIap.setActivity(currentActivity) }
|
|
80
|
+
.onFailure { Log.w(TAG, "initConnection: Activity missing; proceeding headless", it) }
|
|
81
|
+
|
|
82
|
+
// If already connected, short-circuit
|
|
83
|
+
if (connectionReady.get()) {
|
|
84
|
+
promise.resolve(true)
|
|
85
|
+
return@withLock
|
|
86
|
+
}
|
|
149
87
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
88
|
+
// Attach listeners early to avoid races during init
|
|
89
|
+
if (!listenersAttached) {
|
|
90
|
+
listenersAttached = true
|
|
91
|
+
openIap.addPurchaseUpdateListener { p ->
|
|
92
|
+
runCatching { emitOrQueue(EVENT_PURCHASE_UPDATED, p.toJSON()) }
|
|
93
|
+
.onFailure { Log.e(TAG, "Failed to buffer/send PURCHASE_UPDATED", it) }
|
|
94
|
+
}
|
|
95
|
+
openIap.addPurchaseErrorListener { e ->
|
|
96
|
+
runCatching { emitOrQueue(EVENT_PURCHASE_ERROR, e.toJSON()) }
|
|
97
|
+
.onFailure { Log.e(TAG, "Failed to buffer/send PURCHASE_ERROR", it) }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
156
100
|
|
|
157
|
-
|
|
158
|
-
ensureConnection(promise) { billingClient ->
|
|
159
|
-
val skuList =
|
|
160
|
-
skuArr.map { sku ->
|
|
161
|
-
QueryProductDetailsParams.Product
|
|
162
|
-
.newBuilder()
|
|
163
|
-
.setProductId(sku)
|
|
164
|
-
.setProductType(type)
|
|
165
|
-
.build()
|
|
166
|
-
}
|
|
101
|
+
val ok = openIap.initConnection()
|
|
167
102
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
103
|
+
if (!ok) {
|
|
104
|
+
// Clear any buffered events from a failed init
|
|
105
|
+
pendingEvents.clear()
|
|
106
|
+
promise.reject(OpenIapError.E_INIT_CONNECTION, "Failed to initialize connection", null)
|
|
107
|
+
return@withLock
|
|
108
|
+
}
|
|
172
109
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
110
|
+
// Mark ready then flush any buffered events
|
|
111
|
+
connectionReady.set(true)
|
|
112
|
+
while (true) {
|
|
113
|
+
val ev = pendingEvents.poll() ?: break
|
|
114
|
+
// Already on main dispatcher here; emit directly
|
|
115
|
+
runCatching { sendEvent(ev.first, ev.second) }
|
|
116
|
+
.onFailure { Log.e(TAG, "Failed to flush buffered event: ${ev.first}", it) }
|
|
117
|
+
}
|
|
178
118
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
promise.reject(
|
|
182
|
-
IapErrorCode.E_QUERY_PRODUCT,
|
|
183
|
-
"Error querying product details: ${billingResult.debugMessage}",
|
|
184
|
-
null,
|
|
185
|
-
)
|
|
186
|
-
return@queryProductDetailsAsync
|
|
119
|
+
promise.resolve(true)
|
|
120
|
+
} catch (e: Exception) {
|
|
121
|
+
promise.reject(OpenIapError.E_INIT_CONNECTION, e.message, e)
|
|
187
122
|
}
|
|
188
|
-
|
|
189
|
-
val productDetailsList = productDetailsResult.productDetailsList ?: emptyList()
|
|
190
|
-
|
|
191
|
-
val items =
|
|
192
|
-
productDetailsList.map { productDetails ->
|
|
193
|
-
skus[productDetails.productId] = productDetails
|
|
194
|
-
|
|
195
|
-
val currency = productDetails.oneTimePurchaseOfferDetails?.priceCurrencyCode
|
|
196
|
-
?: productDetails.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.priceCurrencyCode
|
|
197
|
-
?: "Unknown"
|
|
198
|
-
val displayPrice = productDetails.oneTimePurchaseOfferDetails?.formattedPrice
|
|
199
|
-
?: productDetails.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.formattedPrice
|
|
200
|
-
?: "N/A"
|
|
201
|
-
|
|
202
|
-
// Prepare reusable data
|
|
203
|
-
val oneTimePurchaseData = productDetails.oneTimePurchaseOfferDetails?.let {
|
|
204
|
-
mapOf(
|
|
205
|
-
"priceCurrencyCode" to it.priceCurrencyCode,
|
|
206
|
-
"formattedPrice" to it.formattedPrice,
|
|
207
|
-
"priceAmountMicros" to it.priceAmountMicros.toString(),
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
val subscriptionOfferData = productDetails.subscriptionOfferDetails?.map { subscriptionOfferDetailsItem ->
|
|
212
|
-
mapOf(
|
|
213
|
-
"basePlanId" to subscriptionOfferDetailsItem.basePlanId,
|
|
214
|
-
"offerId" to subscriptionOfferDetailsItem.offerId,
|
|
215
|
-
"offerToken" to subscriptionOfferDetailsItem.offerToken,
|
|
216
|
-
"offerTags" to subscriptionOfferDetailsItem.offerTags,
|
|
217
|
-
"pricingPhases" to
|
|
218
|
-
mapOf(
|
|
219
|
-
"pricingPhaseList" to
|
|
220
|
-
subscriptionOfferDetailsItem.pricingPhases.pricingPhaseList.map
|
|
221
|
-
{ pricingPhaseItem ->
|
|
222
|
-
mapOf(
|
|
223
|
-
"formattedPrice" to pricingPhaseItem.formattedPrice,
|
|
224
|
-
"priceCurrencyCode" to pricingPhaseItem.priceCurrencyCode,
|
|
225
|
-
"billingPeriod" to pricingPhaseItem.billingPeriod,
|
|
226
|
-
"billingCycleCount" to pricingPhaseItem.billingCycleCount,
|
|
227
|
-
"priceAmountMicros" to
|
|
228
|
-
pricingPhaseItem.priceAmountMicros.toString(),
|
|
229
|
-
"recurrenceMode" to pricingPhaseItem.recurrenceMode,
|
|
230
|
-
)
|
|
231
|
-
},
|
|
232
|
-
),
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Convert Android productType to our expected 'inapp' or 'subs'
|
|
237
|
-
val productType = if (productDetails.productType == BillingClient.ProductType.SUBS) "subs" else "inapp"
|
|
238
|
-
|
|
239
|
-
mapOf(
|
|
240
|
-
"id" to productDetails.productId,
|
|
241
|
-
"title" to productDetails.title,
|
|
242
|
-
"description" to productDetails.description,
|
|
243
|
-
"type" to productType,
|
|
244
|
-
// New field names with Android suffix
|
|
245
|
-
"nameAndroid" to productDetails.name,
|
|
246
|
-
"oneTimePurchaseOfferDetailsAndroid" to oneTimePurchaseData,
|
|
247
|
-
"subscriptionOfferDetailsAndroid" to subscriptionOfferData,
|
|
248
|
-
"platform" to "android",
|
|
249
|
-
"currency" to currency,
|
|
250
|
-
"displayPrice" to displayPrice,
|
|
251
|
-
// START: Deprecated - will be removed in v2.9.0
|
|
252
|
-
// Use nameAndroid instead of displayName
|
|
253
|
-
"displayName" to productDetails.name,
|
|
254
|
-
// Use nameAndroid instead of name
|
|
255
|
-
"name" to productDetails.name,
|
|
256
|
-
// Use oneTimePurchaseOfferDetailsAndroid instead of oneTimePurchaseOfferDetails
|
|
257
|
-
"oneTimePurchaseOfferDetails" to oneTimePurchaseData,
|
|
258
|
-
// Use subscriptionOfferDetailsAndroid instead of subscriptionOfferDetails
|
|
259
|
-
"subscriptionOfferDetails" to subscriptionOfferData,
|
|
260
|
-
// END: Deprecated - will be removed in v2.9.0
|
|
261
|
-
)
|
|
262
|
-
}
|
|
263
|
-
promise.resolve(items)
|
|
264
123
|
}
|
|
265
124
|
}
|
|
266
125
|
}
|
|
267
126
|
|
|
268
|
-
AsyncFunction("
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
.setProductId(sku)
|
|
277
|
-
.setProductType(type)
|
|
278
|
-
.build()
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (skuList.isEmpty()) {
|
|
282
|
-
promise.reject(IapErrorCode.E_EMPTY_SKU_LIST, "The SKU list is empty.", null)
|
|
283
|
-
return@ensureConnection
|
|
127
|
+
AsyncFunction("endConnection") { promise: Promise ->
|
|
128
|
+
scope.launch {
|
|
129
|
+
connectionMutex.withLock {
|
|
130
|
+
runCatching { openIap.endConnection() }
|
|
131
|
+
// Reset connection state and clear any buffered events
|
|
132
|
+
connectionReady.set(false)
|
|
133
|
+
pendingEvents.clear()
|
|
134
|
+
promise.resolve(true)
|
|
284
135
|
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
285
138
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
null,
|
|
298
|
-
)
|
|
299
|
-
return@queryProductDetailsAsync
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
val productDetailsList = productDetailsResult.productDetailsList ?: emptyList()
|
|
303
|
-
|
|
304
|
-
val items =
|
|
305
|
-
productDetailsList.map { productDetails ->
|
|
306
|
-
skus[productDetails.productId] = productDetails
|
|
307
|
-
|
|
308
|
-
val currency = productDetails.oneTimePurchaseOfferDetails?.priceCurrencyCode
|
|
309
|
-
?: productDetails.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.priceCurrencyCode
|
|
310
|
-
?: "Unknown"
|
|
311
|
-
val displayPrice = productDetails.oneTimePurchaseOfferDetails?.formattedPrice
|
|
312
|
-
?: productDetails.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.formattedPrice
|
|
313
|
-
?: "N/A"
|
|
314
|
-
|
|
315
|
-
// Prepare reusable data
|
|
316
|
-
val oneTimePurchaseData = productDetails.oneTimePurchaseOfferDetails?.let {
|
|
317
|
-
mapOf(
|
|
318
|
-
"priceCurrencyCode" to it.priceCurrencyCode,
|
|
319
|
-
"formattedPrice" to it.formattedPrice,
|
|
320
|
-
"priceAmountMicros" to it.priceAmountMicros.toString(),
|
|
321
|
-
)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
val subscriptionOfferData = productDetails.subscriptionOfferDetails?.map { subscriptionOfferDetailsItem ->
|
|
325
|
-
mapOf(
|
|
326
|
-
"basePlanId" to subscriptionOfferDetailsItem.basePlanId,
|
|
327
|
-
"offerId" to subscriptionOfferDetailsItem.offerId,
|
|
328
|
-
"offerToken" to subscriptionOfferDetailsItem.offerToken,
|
|
329
|
-
"offerTags" to subscriptionOfferDetailsItem.offerTags,
|
|
330
|
-
"pricingPhases" to
|
|
331
|
-
mapOf(
|
|
332
|
-
"pricingPhaseList" to
|
|
333
|
-
subscriptionOfferDetailsItem.pricingPhases.pricingPhaseList.map
|
|
334
|
-
{ pricingPhaseItem ->
|
|
335
|
-
mapOf(
|
|
336
|
-
"formattedPrice" to pricingPhaseItem.formattedPrice,
|
|
337
|
-
"priceCurrencyCode" to pricingPhaseItem.priceCurrencyCode,
|
|
338
|
-
"billingPeriod" to pricingPhaseItem.billingPeriod,
|
|
339
|
-
"billingCycleCount" to pricingPhaseItem.billingCycleCount,
|
|
340
|
-
"priceAmountMicros" to
|
|
341
|
-
pricingPhaseItem.priceAmountMicros.toString(),
|
|
342
|
-
"recurrenceMode" to pricingPhaseItem.recurrenceMode,
|
|
343
|
-
)
|
|
344
|
-
},
|
|
345
|
-
),
|
|
346
|
-
)
|
|
347
|
-
}
|
|
139
|
+
AsyncFunction("fetchProducts") { type: String, skuArr: Array<String>, promise: Promise ->
|
|
140
|
+
scope.launch {
|
|
141
|
+
try {
|
|
142
|
+
val reqType = ProductRequest.ProductRequestType.fromString(type)
|
|
143
|
+
val products = openIap.fetchProducts(ProductRequest(skuArr.toList(), reqType))
|
|
144
|
+
promise.resolve(products.map { it.toJSON() })
|
|
145
|
+
} catch (e: Exception) {
|
|
146
|
+
promise.reject(OpenIapError.E_QUERY_PRODUCT, e.message, null)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
348
150
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
"type" to productType,
|
|
357
|
-
// New field names with Android suffix
|
|
358
|
-
"nameAndroid" to productDetails.name,
|
|
359
|
-
"oneTimePurchaseOfferDetailsAndroid" to oneTimePurchaseData,
|
|
360
|
-
"subscriptionOfferDetailsAndroid" to subscriptionOfferData,
|
|
361
|
-
"platform" to "android",
|
|
362
|
-
"currency" to currency,
|
|
363
|
-
"displayPrice" to displayPrice,
|
|
364
|
-
// START: Deprecated - will be removed in v2.9.0
|
|
365
|
-
// Use nameAndroid instead of displayName
|
|
366
|
-
"displayName" to productDetails.name,
|
|
367
|
-
// Use nameAndroid instead of name
|
|
368
|
-
"name" to productDetails.name,
|
|
369
|
-
// Use oneTimePurchaseOfferDetailsAndroid instead of oneTimePurchaseOfferDetails
|
|
370
|
-
"oneTimePurchaseOfferDetails" to oneTimePurchaseData,
|
|
371
|
-
// Use subscriptionOfferDetailsAndroid instead of subscriptionOfferDetails
|
|
372
|
-
"subscriptionOfferDetails" to subscriptionOfferData,
|
|
373
|
-
// END: Deprecated - will be removed in v2.9.0
|
|
374
|
-
)
|
|
375
|
-
}
|
|
376
|
-
promise.resolve(items)
|
|
151
|
+
AsyncFunction("getAvailableItems") { promise: Promise ->
|
|
152
|
+
scope.launch {
|
|
153
|
+
try {
|
|
154
|
+
val purchases = openIap.getAvailablePurchases(null)
|
|
155
|
+
promise.resolve(purchases.map { it.toJSON() })
|
|
156
|
+
} catch (e: Exception) {
|
|
157
|
+
promise.reject(OpenIapError.E_SERVICE_ERROR, e.message, null)
|
|
377
158
|
}
|
|
378
159
|
}
|
|
379
160
|
}
|
|
380
161
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
392
|
-
purchases?.forEach { purchase ->
|
|
393
|
-
val item =
|
|
394
|
-
mutableMapOf<String, Any?>(
|
|
395
|
-
"id" to purchase.orderId,
|
|
396
|
-
"productId" to purchase.products.firstOrNull() as Any?,
|
|
397
|
-
"ids" to purchase.products,
|
|
398
|
-
"transactionId" to purchase.orderId, // @deprecated - use id instead
|
|
399
|
-
"transactionDate" to purchase.purchaseTime.toDouble(),
|
|
400
|
-
"transactionReceipt" to purchase.originalJson,
|
|
401
|
-
"orderId" to purchase.orderId,
|
|
402
|
-
"purchaseTokenAndroid" to purchase.purchaseToken,
|
|
403
|
-
"purchaseToken" to purchase.purchaseToken,
|
|
404
|
-
"developerPayloadAndroid" to purchase.developerPayload,
|
|
405
|
-
"signatureAndroid" to purchase.signature,
|
|
406
|
-
"purchaseStateAndroid" to purchase.purchaseState,
|
|
407
|
-
"isAcknowledgedAndroid" to purchase.isAcknowledged,
|
|
408
|
-
"packageNameAndroid" to purchase.packageName,
|
|
409
|
-
"obfuscatedAccountIdAndroid" to purchase.accountIdentifiers?.obfuscatedAccountId,
|
|
410
|
-
"obfuscatedProfileIdAndroid" to purchase.accountIdentifiers?.obfuscatedProfileId,
|
|
411
|
-
"platform" to "android",
|
|
412
|
-
)
|
|
413
|
-
if (type == BillingClient.ProductType.SUBS) {
|
|
414
|
-
item["autoRenewingAndroid"] = purchase.isAutoRenewing
|
|
415
|
-
}
|
|
416
|
-
items.add(item)
|
|
417
|
-
}
|
|
418
|
-
promise.resolve(items)
|
|
162
|
+
// Deep link to Manage Subscriptions screen (Android)
|
|
163
|
+
AsyncFunction("deepLinkToSubscriptionsAndroid") { params: Map<String, Any?>, promise: Promise ->
|
|
164
|
+
val sku = (params["sku"] ?: params["skuAndroid"]) as? String
|
|
165
|
+
val packageName = (params["packageName"] ?: params["packageNameAndroid"]) as? String
|
|
166
|
+
scope.launch {
|
|
167
|
+
try {
|
|
168
|
+
openIap.deepLinkToSubscriptions(DeepLinkOptions(sku, packageName))
|
|
169
|
+
promise.resolve(null)
|
|
170
|
+
} catch (e: Exception) {
|
|
171
|
+
promise.reject(OpenIapError.E_SERVICE_ERROR, e.message, null)
|
|
419
172
|
}
|
|
420
173
|
}
|
|
421
174
|
}
|
|
422
175
|
|
|
423
|
-
//
|
|
424
|
-
|
|
176
|
+
// Get Google Play storefront country code (Android)
|
|
177
|
+
AsyncFunction("getStorefrontAndroid") { promise: Promise ->
|
|
178
|
+
scope.launch {
|
|
179
|
+
try {
|
|
180
|
+
val code = openIap.getStorefront()
|
|
181
|
+
promise.resolve(code)
|
|
182
|
+
} catch (e: Exception) {
|
|
183
|
+
promise.reject(OpenIapError.E_SERVICE_ERROR, e.message, e)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
425
187
|
|
|
426
188
|
AsyncFunction("requestPurchase") { params: Map<String, Any?>, promise: Promise ->
|
|
427
189
|
val type = params["type"] as String
|
|
428
|
-
val
|
|
429
|
-
(params["
|
|
430
|
-
?:
|
|
431
|
-
|
|
432
|
-
val
|
|
433
|
-
|
|
434
|
-
val obfuscatedProfileId =
|
|
435
|
-
|
|
436
|
-
(params["offerTokenArr"] as? List<*>)
|
|
437
|
-
?.filterIsInstance<String>()
|
|
438
|
-
?.toTypedArray() ?: emptyArray()
|
|
190
|
+
val skus: List<String> =
|
|
191
|
+
(params["skus"] as? List<*>)?.filterIsInstance<String>()
|
|
192
|
+
?: (params["skuArr"] as? List<*>)?.filterIsInstance<String>()
|
|
193
|
+
?: emptyList()
|
|
194
|
+
val obfuscatedAccountId =
|
|
195
|
+
(params["obfuscatedAccountIdAndroid"] ?: params["obfuscatedAccountId"]) as? String
|
|
196
|
+
val obfuscatedProfileId =
|
|
197
|
+
(params["obfuscatedProfileIdAndroid"] ?: params["obfuscatedProfileId"]) as? String
|
|
439
198
|
val isOfferPersonalized = params["isOfferPersonalized"] as? Boolean ?: false
|
|
440
199
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
"debugMessage" to debugMessage,
|
|
456
|
-
"code" to IapErrorCode.E_SKU_OFFER_MISMATCH,
|
|
457
|
-
"message" to debugMessage,
|
|
458
|
-
)
|
|
200
|
+
PromiseUtils.addPromiseForKey(PromiseUtils.PROMISE_BUY_ITEM, promise)
|
|
201
|
+
scope.launch {
|
|
202
|
+
try {
|
|
203
|
+
openIap.setActivity(currentActivity)
|
|
204
|
+
val reqType = ProductRequest.ProductRequestType.fromString(type)
|
|
205
|
+
val result =
|
|
206
|
+
openIap.requestPurchase(
|
|
207
|
+
RequestPurchaseAndroidProps(
|
|
208
|
+
skus = skus,
|
|
209
|
+
obfuscatedAccountIdAndroid = obfuscatedAccountId,
|
|
210
|
+
obfuscatedProfileIdAndroid = obfuscatedProfileId,
|
|
211
|
+
isOfferPersonalized = isOfferPersonalized,
|
|
212
|
+
),
|
|
213
|
+
reqType,
|
|
459
214
|
)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
val productParamsList =
|
|
468
|
-
skuArr.mapIndexed { index, sku ->
|
|
469
|
-
val selectedSku = skus[sku]
|
|
470
|
-
if (selectedSku == null) {
|
|
471
|
-
val debugMessage =
|
|
472
|
-
"The sku was not found. Please fetch products first by calling getItems"
|
|
473
|
-
try {
|
|
474
|
-
sendEvent(
|
|
475
|
-
OpenIapEvent.PURCHASE_ERROR,
|
|
476
|
-
mapOf(
|
|
477
|
-
"debugMessage" to debugMessage,
|
|
478
|
-
"code" to IapErrorCode.E_SKU_NOT_FOUND,
|
|
479
|
-
"message" to debugMessage,
|
|
480
|
-
"productId" to sku,
|
|
481
|
-
),
|
|
482
|
-
)
|
|
483
|
-
} catch (e: Exception) {
|
|
484
|
-
Log.e(TAG, "Failed to send PURCHASE_ERROR event: ${e.message}")
|
|
485
|
-
}
|
|
486
|
-
promise.reject(IapErrorCode.E_SKU_NOT_FOUND, debugMessage, null)
|
|
487
|
-
return@ensureConnection
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
val productDetailParams =
|
|
491
|
-
BillingFlowParams.ProductDetailsParams
|
|
492
|
-
.newBuilder()
|
|
493
|
-
.setProductDetails(selectedSku)
|
|
494
|
-
|
|
495
|
-
if (type == BillingClient.ProductType.SUBS) {
|
|
496
|
-
productDetailParams.setOfferToken(offerTokenArr[index])
|
|
215
|
+
result.forEach { p ->
|
|
216
|
+
try {
|
|
217
|
+
emitOrQueue(EVENT_PURCHASE_UPDATED, p.toJSON())
|
|
218
|
+
} catch (ex: Exception) {
|
|
219
|
+
Log.e(TAG, "Failed to send PURCHASE_UPDATED event (requestPurchase)", ex)
|
|
497
220
|
}
|
|
498
|
-
|
|
499
|
-
productDetailParams.build()
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
val builder =
|
|
503
|
-
BillingFlowParams
|
|
504
|
-
.newBuilder()
|
|
505
|
-
.setProductDetailsParamsList(productParamsList)
|
|
506
|
-
.setIsOfferPersonalized(isOfferPersonalized)
|
|
507
|
-
|
|
508
|
-
if (purchaseToken != null) {
|
|
509
|
-
val subscriptionUpdateParams =
|
|
510
|
-
SubscriptionUpdateParams
|
|
511
|
-
.newBuilder()
|
|
512
|
-
.setOldPurchaseToken(purchaseToken)
|
|
513
|
-
|
|
514
|
-
if (type == BillingClient.ProductType.SUBS && replacementMode != -1) {
|
|
515
|
-
val mode =
|
|
516
|
-
when (replacementMode) {
|
|
517
|
-
SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE ->
|
|
518
|
-
SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE
|
|
519
|
-
SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION ->
|
|
520
|
-
SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION
|
|
521
|
-
SubscriptionUpdateParams.ReplacementMode.DEFERRED ->
|
|
522
|
-
SubscriptionUpdateParams.ReplacementMode.DEFERRED
|
|
523
|
-
SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION ->
|
|
524
|
-
SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION
|
|
525
|
-
SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE ->
|
|
526
|
-
SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE
|
|
527
|
-
else -> SubscriptionUpdateParams.ReplacementMode.UNKNOWN_REPLACEMENT_MODE
|
|
528
|
-
}
|
|
529
|
-
subscriptionUpdateParams.setSubscriptionReplacementMode(mode)
|
|
530
221
|
}
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
541
|
-
val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
|
|
542
|
-
var errorMessage = billingResult.debugMessage ?: errorData.message
|
|
543
|
-
var subResponseCode: Int? = null
|
|
544
|
-
|
|
545
|
-
// Check for sub-response codes (v8.0.0+)
|
|
222
|
+
PromiseUtils.resolvePromisesForKey(PromiseUtils.PROMISE_BUY_ITEM, result.map { it.toJSON() })
|
|
223
|
+
} catch (e: Exception) {
|
|
224
|
+
val errorMap =
|
|
225
|
+
mapOf(
|
|
226
|
+
"code" to OpenIapError.E_PURCHASE_ERROR,
|
|
227
|
+
"message" to (e.message ?: "Purchase failed"),
|
|
228
|
+
"platform" to "android",
|
|
229
|
+
)
|
|
546
230
|
try {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
errorMessage = "$errorMessage (Payment declined due to insufficient funds)"
|
|
551
|
-
} else {
|
|
552
|
-
errorMessage = "$errorMessage (Sub-response code: $subResponseCode)"
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
} catch (e: Exception) {
|
|
556
|
-
// Method doesn't exist in older versions, ignore
|
|
231
|
+
emitOrQueue(EVENT_PURCHASE_ERROR, errorMap)
|
|
232
|
+
} catch (ex: Exception) {
|
|
233
|
+
Log.e(TAG, "Failed to send PURCHASE_ERROR event (requestPurchase)", ex)
|
|
557
234
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
"message" to errorMessage
|
|
235
|
+
// Reject and clear any pending promises for this purchase flow
|
|
236
|
+
PromiseUtils.rejectPromisesForKey(
|
|
237
|
+
PromiseUtils.PROMISE_BUY_ITEM,
|
|
238
|
+
OpenIapError.E_PURCHASE_ERROR,
|
|
239
|
+
e.message,
|
|
240
|
+
null,
|
|
565
241
|
)
|
|
566
|
-
|
|
567
|
-
// Add product ID if available
|
|
568
|
-
if (skuArr.isNotEmpty()) {
|
|
569
|
-
errorMap["productId"] = skuArr.first()
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Add sub-response code if available
|
|
573
|
-
subResponseCode?.let {
|
|
574
|
-
if (it != 0) {
|
|
575
|
-
errorMap["subResponseCode"] = it
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
try {
|
|
580
|
-
sendEvent(OpenIapEvent.PURCHASE_ERROR, errorMap.toMap())
|
|
581
|
-
} catch (e: Exception) {
|
|
582
|
-
Log.e(TAG, "Failed to send PURCHASE_ERROR event: ${e.message}")
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
promise.reject(errorData.code, errorMessage, null)
|
|
586
|
-
return@ensureConnection
|
|
587
242
|
}
|
|
588
243
|
}
|
|
589
244
|
}
|
|
590
245
|
|
|
591
|
-
AsyncFunction("acknowledgePurchaseAndroid") {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
AcknowledgePurchaseParams
|
|
599
|
-
.newBuilder()
|
|
600
|
-
.setPurchaseToken(token)
|
|
601
|
-
.build()
|
|
602
|
-
|
|
603
|
-
billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult: BillingResult ->
|
|
604
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
605
|
-
PlayUtils.rejectPromiseWithBillingError(
|
|
606
|
-
promise,
|
|
607
|
-
billingResult.responseCode,
|
|
608
|
-
)
|
|
609
|
-
return@acknowledgePurchase
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
val map = mutableMapOf<String, Any?>()
|
|
613
|
-
map["responseCode"] = billingResult.responseCode
|
|
614
|
-
map["debugMessage"] = billingResult.debugMessage
|
|
615
|
-
val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
|
|
616
|
-
map["code"] = errorData.code
|
|
617
|
-
map["message"] = errorData.message
|
|
618
|
-
promise.resolve(map)
|
|
246
|
+
AsyncFunction("acknowledgePurchaseAndroid") { token: String, promise: Promise ->
|
|
247
|
+
scope.launch {
|
|
248
|
+
try {
|
|
249
|
+
openIap.acknowledgePurchaseAndroid(token)
|
|
250
|
+
promise.resolve(mapOf("responseCode" to 0))
|
|
251
|
+
} catch (e: Exception) {
|
|
252
|
+
promise.reject(OpenIapError.E_SERVICE_ERROR, e.message, null)
|
|
619
253
|
}
|
|
620
254
|
}
|
|
621
255
|
}
|
|
622
256
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
billingClient.consumeAsync(params) { billingResult: BillingResult, purchaseToken: String? ->
|
|
632
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
633
|
-
PlayUtils.rejectPromiseWithBillingError(
|
|
634
|
-
promise,
|
|
635
|
-
billingResult.responseCode,
|
|
636
|
-
)
|
|
637
|
-
return@consumeAsync
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
val map = mutableMapOf<String, Any?>()
|
|
641
|
-
map["responseCode"] = billingResult.responseCode
|
|
642
|
-
map["debugMessage"] = billingResult.debugMessage
|
|
643
|
-
val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
|
|
644
|
-
map["code"] = errorData.code
|
|
645
|
-
map["message"] = errorData.message
|
|
646
|
-
map["purchaseTokenAndroid"] = purchaseToken
|
|
647
|
-
promise.resolve(map)
|
|
257
|
+
// New name: consumePurchaseAndroid
|
|
258
|
+
AsyncFunction("consumePurchaseAndroid") { token: String, promise: Promise ->
|
|
259
|
+
scope.launch {
|
|
260
|
+
try {
|
|
261
|
+
openIap.consumePurchaseAndroid(token)
|
|
262
|
+
promise.resolve(mapOf("responseCode" to 0, "purchaseToken" to token))
|
|
263
|
+
} catch (e: Exception) {
|
|
264
|
+
promise.reject(OpenIapError.E_SERVICE_ERROR, e.message, null)
|
|
648
265
|
}
|
|
649
266
|
}
|
|
650
267
|
}
|
|
651
268
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
promise: Promise,
|
|
655
|
-
->
|
|
656
|
-
ensureConnection(promise) { billingClient ->
|
|
657
|
-
billingClient.getBillingConfigAsync(
|
|
658
|
-
GetBillingConfigParams.newBuilder().build(),
|
|
659
|
-
BillingConfigResponseListener { result: BillingResult, config: BillingConfig? ->
|
|
660
|
-
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
661
|
-
promise.safeResolve(config?.countryCode.orEmpty())
|
|
662
|
-
} else {
|
|
663
|
-
val debugMessage = result.debugMessage.orEmpty()
|
|
664
|
-
promise.safeReject(result.responseCode.toString(), debugMessage)
|
|
665
|
-
}
|
|
666
|
-
},
|
|
667
|
-
)
|
|
668
|
-
}
|
|
269
|
+
OnDestroy {
|
|
270
|
+
job.cancel()
|
|
669
271
|
}
|
|
670
272
|
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Rejects promise with billing code if BillingResult is not OK
|
|
674
|
-
*/
|
|
675
|
-
private fun isValidResult(
|
|
676
|
-
billingResult: BillingResult,
|
|
677
|
-
promise: Promise,
|
|
678
|
-
): Boolean {
|
|
679
|
-
Log.d(TAG, "responseCode: " + billingResult.responseCode)
|
|
680
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
681
|
-
PlayUtils.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
682
|
-
return false
|
|
683
|
-
}
|
|
684
|
-
return true
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
private fun ensureConnection(
|
|
688
|
-
promise: Promise,
|
|
689
|
-
callback: (billingClient: BillingClient) -> Unit,
|
|
690
|
-
) {
|
|
691
|
-
// With auto-reconnection enabled, we only need to check if billing client exists
|
|
692
|
-
// The service will automatically reconnect when needed
|
|
693
|
-
if (billingClientCache != null) {
|
|
694
|
-
callback(billingClientCache!!)
|
|
695
|
-
return
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
initBillingClient(promise, callback)
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
private fun initBillingClient(
|
|
702
|
-
promise: Promise,
|
|
703
|
-
callback: (billingClient: BillingClient) -> Unit,
|
|
704
|
-
) {
|
|
705
|
-
if (GoogleApiAvailability
|
|
706
|
-
.getInstance()
|
|
707
|
-
.isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS
|
|
708
|
-
) {
|
|
709
|
-
Log.i(TAG, "Google Play Services are not available on this device")
|
|
710
|
-
promise.reject(
|
|
711
|
-
IapErrorCode.E_NOT_PREPARED,
|
|
712
|
-
"Google Play Services are not available on this device",
|
|
713
|
-
null,
|
|
714
|
-
)
|
|
715
|
-
return
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
billingClientCache =
|
|
719
|
-
BillingClient
|
|
720
|
-
.newBuilder(context)
|
|
721
|
-
.setListener(this)
|
|
722
|
-
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
|
|
723
|
-
.enableAutoServiceReconnection() // Automatically handle service disconnections
|
|
724
|
-
.build()
|
|
725
|
-
|
|
726
|
-
billingClientCache?.startConnection(
|
|
727
|
-
object : BillingClientStateListener {
|
|
728
|
-
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
729
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
730
|
-
promise.reject(
|
|
731
|
-
IapErrorCode.E_INIT_CONNECTION,
|
|
732
|
-
"Billing setup finished with error: ${billingResult.debugMessage}",
|
|
733
|
-
null,
|
|
734
|
-
)
|
|
735
|
-
return
|
|
736
|
-
}
|
|
737
|
-
callback(billingClientCache!!)
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
override fun onBillingServiceDisconnected() {
|
|
741
|
-
Log.i(TAG, "Billing service disconnected")
|
|
742
|
-
}
|
|
743
|
-
},
|
|
744
|
-
)
|
|
745
|
-
}
|
|
746
273
|
}
|