expo-iap 2.9.7 → 3.0.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 +36 -0
- package/README.md +10 -2
- package/android/build.gradle +7 -2
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +195 -650
- 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 +10 -3
- package/jest.config.js +5 -9
- package/package.json +5 -3
- package/plugin/build/withIAP.d.ts +4 -1
- package/plugin/build/withIAP.js +48 -28
- package/plugin/build/withLocalOpenIAP.d.ts +6 -2
- package/plugin/build/withLocalOpenIAP.js +179 -20
- package/plugin/src/withIAP.ts +81 -37
- package/plugin/src/withLocalOpenIAP.ts +232 -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,727 +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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
val skuList =
|
|
161
|
-
skuArr.map { sku ->
|
|
162
|
-
QueryProductDetailsParams.Product
|
|
163
|
-
.newBuilder()
|
|
164
|
-
.setProductId(sku)
|
|
165
|
-
.setProductType(type)
|
|
166
|
-
.build()
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (skuList.isEmpty()) {
|
|
170
|
-
promise.reject(IapErrorCode.E_EMPTY_SKU_LIST, "The SKU list is empty.", null)
|
|
171
|
-
return@AsyncFunction
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
val params =
|
|
175
|
-
QueryProductDetailsParams
|
|
176
|
-
.newBuilder()
|
|
177
|
-
.setProductList(skuList)
|
|
178
|
-
.build()
|
|
179
|
-
|
|
180
|
-
billingClient.queryProductDetailsAsync(params) { billingResult: BillingResult, productDetailsResult: QueryProductDetailsResult ->
|
|
181
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
182
|
-
promise.reject(
|
|
183
|
-
IapErrorCode.E_QUERY_PRODUCT,
|
|
184
|
-
"Error querying product details: ${billingResult.debugMessage}",
|
|
185
|
-
null,
|
|
186
|
-
)
|
|
187
|
-
return@queryProductDetailsAsync
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
val productDetailsList = productDetailsResult.productDetailsList ?: emptyList()
|
|
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
|
+
}
|
|
191
87
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
}
|
|
195
100
|
|
|
196
|
-
val
|
|
197
|
-
?: productDetails.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.priceCurrencyCode
|
|
198
|
-
?: "Unknown"
|
|
199
|
-
val displayPrice = productDetails.oneTimePurchaseOfferDetails?.formattedPrice
|
|
200
|
-
?: productDetails.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.formattedPrice
|
|
201
|
-
?: "N/A"
|
|
101
|
+
val ok = openIap.initConnection()
|
|
202
102
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
"priceAmountMicros" to it.priceAmountMicros.toString(),
|
|
209
|
-
)
|
|
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
|
|
210
108
|
}
|
|
211
109
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
"
|
|
219
|
-
mapOf(
|
|
220
|
-
"pricingPhaseList" to
|
|
221
|
-
subscriptionOfferDetailsItem.pricingPhases.pricingPhaseList.map
|
|
222
|
-
{ pricingPhaseItem ->
|
|
223
|
-
mapOf(
|
|
224
|
-
"formattedPrice" to pricingPhaseItem.formattedPrice,
|
|
225
|
-
"priceCurrencyCode" to pricingPhaseItem.priceCurrencyCode,
|
|
226
|
-
"billingPeriod" to pricingPhaseItem.billingPeriod,
|
|
227
|
-
"billingCycleCount" to pricingPhaseItem.billingCycleCount,
|
|
228
|
-
"priceAmountMicros" to
|
|
229
|
-
pricingPhaseItem.priceAmountMicros.toString(),
|
|
230
|
-
"recurrenceMode" to pricingPhaseItem.recurrenceMode,
|
|
231
|
-
)
|
|
232
|
-
},
|
|
233
|
-
),
|
|
234
|
-
)
|
|
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) }
|
|
235
117
|
}
|
|
236
118
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
mapOf(
|
|
241
|
-
"id" to productDetails.productId,
|
|
242
|
-
"title" to productDetails.title,
|
|
243
|
-
"description" to productDetails.description,
|
|
244
|
-
"type" to productType,
|
|
245
|
-
// New field names with Android suffix
|
|
246
|
-
"nameAndroid" to productDetails.name,
|
|
247
|
-
"oneTimePurchaseOfferDetailsAndroid" to oneTimePurchaseData,
|
|
248
|
-
"subscriptionOfferDetailsAndroid" to subscriptionOfferData,
|
|
249
|
-
"platform" to "android",
|
|
250
|
-
"currency" to currency,
|
|
251
|
-
"displayPrice" to displayPrice,
|
|
252
|
-
|
|
253
|
-
)
|
|
119
|
+
promise.resolve(true)
|
|
120
|
+
} catch (e: Exception) {
|
|
121
|
+
promise.reject(OpenIapError.E_INIT_CONNECTION, e.message, e)
|
|
254
122
|
}
|
|
255
|
-
|
|
123
|
+
}
|
|
256
124
|
}
|
|
257
125
|
}
|
|
258
126
|
|
|
259
|
-
AsyncFunction("
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
.setProductId(sku)
|
|
268
|
-
.setProductType(type)
|
|
269
|
-
.build()
|
|
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)
|
|
270
135
|
}
|
|
271
|
-
|
|
272
|
-
if (skuList.isEmpty()) {
|
|
273
|
-
promise.reject(IapErrorCode.E_EMPTY_SKU_LIST, "The SKU list is empty.", null)
|
|
274
|
-
return@AsyncFunction
|
|
275
136
|
}
|
|
137
|
+
}
|
|
276
138
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
.
|
|
281
|
-
.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
promise.reject(
|
|
286
|
-
IapErrorCode.E_QUERY_PRODUCT,
|
|
287
|
-
"Error querying product details: ${billingResult.debugMessage}",
|
|
288
|
-
null,
|
|
289
|
-
)
|
|
290
|
-
return@queryProductDetailsAsync
|
|
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)
|
|
291
147
|
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
292
150
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
?: "Unknown"
|
|
302
|
-
val displayPrice = productDetails.oneTimePurchaseOfferDetails?.formattedPrice
|
|
303
|
-
?: productDetails.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.formattedPrice
|
|
304
|
-
?: "N/A"
|
|
305
|
-
|
|
306
|
-
// Prepare reusable data
|
|
307
|
-
val oneTimePurchaseData = productDetails.oneTimePurchaseOfferDetails?.let {
|
|
308
|
-
mapOf(
|
|
309
|
-
"priceCurrencyCode" to it.priceCurrencyCode,
|
|
310
|
-
"formattedPrice" to it.formattedPrice,
|
|
311
|
-
"priceAmountMicros" to it.priceAmountMicros.toString(),
|
|
312
|
-
)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
val subscriptionOfferData = productDetails.subscriptionOfferDetails?.map { subscriptionOfferDetailsItem ->
|
|
316
|
-
mapOf(
|
|
317
|
-
"basePlanId" to subscriptionOfferDetailsItem.basePlanId,
|
|
318
|
-
"offerId" to subscriptionOfferDetailsItem.offerId,
|
|
319
|
-
"offerToken" to subscriptionOfferDetailsItem.offerToken,
|
|
320
|
-
"offerTags" to subscriptionOfferDetailsItem.offerTags,
|
|
321
|
-
"pricingPhases" to
|
|
322
|
-
mapOf(
|
|
323
|
-
"pricingPhaseList" to
|
|
324
|
-
subscriptionOfferDetailsItem.pricingPhases.pricingPhaseList.map
|
|
325
|
-
{ pricingPhaseItem ->
|
|
326
|
-
mapOf(
|
|
327
|
-
"formattedPrice" to pricingPhaseItem.formattedPrice,
|
|
328
|
-
"priceCurrencyCode" to pricingPhaseItem.priceCurrencyCode,
|
|
329
|
-
"billingPeriod" to pricingPhaseItem.billingPeriod,
|
|
330
|
-
"billingCycleCount" to pricingPhaseItem.billingCycleCount,
|
|
331
|
-
"priceAmountMicros" to
|
|
332
|
-
pricingPhaseItem.priceAmountMicros.toString(),
|
|
333
|
-
"recurrenceMode" to pricingPhaseItem.recurrenceMode,
|
|
334
|
-
)
|
|
335
|
-
},
|
|
336
|
-
),
|
|
337
|
-
)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Convert Android productType to our expected 'inapp' or 'subs'
|
|
341
|
-
val productType = if (productDetails.productType == BillingClient.ProductType.SUBS) "subs" else "inapp"
|
|
342
|
-
|
|
343
|
-
mapOf(
|
|
344
|
-
"id" to productDetails.productId,
|
|
345
|
-
"title" to productDetails.title,
|
|
346
|
-
"description" to productDetails.description,
|
|
347
|
-
"type" to productType,
|
|
348
|
-
// New field names with Android suffix
|
|
349
|
-
"nameAndroid" to productDetails.name,
|
|
350
|
-
"oneTimePurchaseOfferDetailsAndroid" to oneTimePurchaseData,
|
|
351
|
-
"subscriptionOfferDetailsAndroid" to subscriptionOfferData,
|
|
352
|
-
"platform" to "android",
|
|
353
|
-
"currency" to currency,
|
|
354
|
-
"displayPrice" to displayPrice,
|
|
355
|
-
|
|
356
|
-
)
|
|
357
|
-
}
|
|
358
|
-
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)
|
|
158
|
+
}
|
|
359
159
|
}
|
|
360
160
|
}
|
|
361
161
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
val
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
373
|
-
purchases?.forEach { purchase ->
|
|
374
|
-
val item =
|
|
375
|
-
mutableMapOf<String, Any?>(
|
|
376
|
-
"id" to purchase.orderId,
|
|
377
|
-
"productId" to purchase.products.firstOrNull() as Any?,
|
|
378
|
-
"ids" to purchase.products,
|
|
379
|
-
"transactionId" to purchase.orderId, // @deprecated - use id instead
|
|
380
|
-
"transactionDate" to purchase.purchaseTime.toDouble(),
|
|
381
|
-
"transactionReceipt" to purchase.originalJson,
|
|
382
|
-
"orderId" to purchase.orderId,
|
|
383
|
-
"purchaseTokenAndroid" to purchase.purchaseToken,
|
|
384
|
-
"purchaseToken" to purchase.purchaseToken,
|
|
385
|
-
"developerPayloadAndroid" to purchase.developerPayload,
|
|
386
|
-
"signatureAndroid" to purchase.signature,
|
|
387
|
-
"purchaseStateAndroid" to purchase.purchaseState,
|
|
388
|
-
"isAcknowledgedAndroid" to purchase.isAcknowledged,
|
|
389
|
-
"packageNameAndroid" to purchase.packageName,
|
|
390
|
-
"obfuscatedAccountIdAndroid" to purchase.accountIdentifiers?.obfuscatedAccountId,
|
|
391
|
-
"obfuscatedProfileIdAndroid" to purchase.accountIdentifiers?.obfuscatedProfileId,
|
|
392
|
-
"platform" to "android",
|
|
393
|
-
)
|
|
394
|
-
if (type == BillingClient.ProductType.SUBS) {
|
|
395
|
-
item["autoRenewingAndroid"] = purchase.isAutoRenewing
|
|
396
|
-
}
|
|
397
|
-
items.add(item)
|
|
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)
|
|
398
172
|
}
|
|
399
|
-
promise.resolve(items)
|
|
400
173
|
}
|
|
401
174
|
}
|
|
402
175
|
|
|
403
|
-
//
|
|
404
|
-
|
|
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
|
+
}
|
|
405
187
|
|
|
406
188
|
AsyncFunction("requestPurchase") { params: Map<String, Any?>, promise: Promise ->
|
|
407
189
|
val type = params["type"] as String
|
|
408
|
-
val
|
|
409
|
-
(params["
|
|
410
|
-
?:
|
|
411
|
-
|
|
412
|
-
val
|
|
413
|
-
|
|
414
|
-
val obfuscatedProfileId =
|
|
415
|
-
|
|
416
|
-
(params["offerTokenArr"] as? List<*>)
|
|
417
|
-
?.filterIsInstance<String>()
|
|
418
|
-
?.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
|
|
419
198
|
val isOfferPersonalized = params["isOfferPersonalized"] as? Boolean ?: false
|
|
420
199
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
"debugMessage" to debugMessage,
|
|
436
|
-
"code" to IapErrorCode.E_SKU_OFFER_MISMATCH,
|
|
437
|
-
"message" to debugMessage,
|
|
438
|
-
)
|
|
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,
|
|
439
214
|
)
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
val productParamsList =
|
|
448
|
-
skuArr.mapIndexed { index, sku ->
|
|
449
|
-
val selectedSku = skus[sku]
|
|
450
|
-
if (selectedSku == null) {
|
|
451
|
-
val debugMessage =
|
|
452
|
-
"The sku was not found. Please fetch products first by calling getItems"
|
|
453
|
-
try {
|
|
454
|
-
sendEvent(
|
|
455
|
-
OpenIapEvent.PURCHASE_ERROR,
|
|
456
|
-
mapOf(
|
|
457
|
-
"debugMessage" to debugMessage,
|
|
458
|
-
"code" to IapErrorCode.E_SKU_NOT_FOUND,
|
|
459
|
-
"message" to debugMessage,
|
|
460
|
-
"productId" to sku,
|
|
461
|
-
),
|
|
462
|
-
)
|
|
463
|
-
} catch (e: Exception) {
|
|
464
|
-
Log.e(TAG, "Failed to send PURCHASE_ERROR event: ${e.message}")
|
|
465
|
-
}
|
|
466
|
-
promise.reject(IapErrorCode.E_SKU_NOT_FOUND, debugMessage, null)
|
|
467
|
-
return@AsyncFunction
|
|
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)
|
|
468
220
|
}
|
|
469
|
-
|
|
470
|
-
val productDetailParams =
|
|
471
|
-
BillingFlowParams.ProductDetailsParams
|
|
472
|
-
.newBuilder()
|
|
473
|
-
.setProductDetails(selectedSku)
|
|
474
|
-
|
|
475
|
-
if (type == BillingClient.ProductType.SUBS) {
|
|
476
|
-
productDetailParams.setOfferToken(offerTokenArr[index])
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
productDetailParams.build()
|
|
480
221
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
val subscriptionUpdateParams =
|
|
490
|
-
SubscriptionUpdateParams
|
|
491
|
-
.newBuilder()
|
|
492
|
-
.setOldPurchaseToken(purchaseToken)
|
|
493
|
-
|
|
494
|
-
if (type == BillingClient.ProductType.SUBS && replacementMode != -1) {
|
|
495
|
-
val mode =
|
|
496
|
-
when (replacementMode) {
|
|
497
|
-
SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE ->
|
|
498
|
-
SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE
|
|
499
|
-
SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION ->
|
|
500
|
-
SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION
|
|
501
|
-
SubscriptionUpdateParams.ReplacementMode.DEFERRED ->
|
|
502
|
-
SubscriptionUpdateParams.ReplacementMode.DEFERRED
|
|
503
|
-
SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION ->
|
|
504
|
-
SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION
|
|
505
|
-
SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE ->
|
|
506
|
-
SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE
|
|
507
|
-
else -> SubscriptionUpdateParams.ReplacementMode.UNKNOWN_REPLACEMENT_MODE
|
|
508
|
-
}
|
|
509
|
-
subscriptionUpdateParams.setSubscriptionReplacementMode(mode)
|
|
510
|
-
}
|
|
511
|
-
builder.setSubscriptionUpdateParams(subscriptionUpdateParams.build())
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
obfuscatedAccountId?.let { builder.setObfuscatedAccountId(it) }
|
|
515
|
-
obfuscatedProfileId?.let { builder.setObfuscatedProfileId(it) }
|
|
516
|
-
|
|
517
|
-
val flowParams = builder.build()
|
|
518
|
-
val billingResult = billingClient.launchBillingFlow(currentActivity, flowParams)
|
|
519
|
-
|
|
520
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
521
|
-
val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
|
|
522
|
-
var errorMessage = billingResult.debugMessage ?: errorData.message
|
|
523
|
-
var subResponseCode: Int? = null
|
|
524
|
-
|
|
525
|
-
// 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
|
+
)
|
|
526
230
|
try {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
errorMessage = "$errorMessage (Payment declined due to insufficient funds)"
|
|
531
|
-
} else {
|
|
532
|
-
errorMessage = "$errorMessage (Sub-response code: $subResponseCode)"
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
} catch (e: Exception) {
|
|
536
|
-
// 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)
|
|
537
234
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
"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,
|
|
545
241
|
)
|
|
546
|
-
|
|
547
|
-
// Add product ID if available
|
|
548
|
-
if (skuArr.isNotEmpty()) {
|
|
549
|
-
errorMap["productId"] = skuArr.first()
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Add sub-response code if available
|
|
553
|
-
subResponseCode?.let {
|
|
554
|
-
if (it != 0) {
|
|
555
|
-
errorMap["subResponseCode"] = it
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
try {
|
|
560
|
-
sendEvent(OpenIapEvent.PURCHASE_ERROR, errorMap.toMap())
|
|
561
|
-
} catch (e: Exception) {
|
|
562
|
-
Log.e(TAG, "Failed to send PURCHASE_ERROR event: ${e.message}")
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
promise.reject(errorData.code, errorMessage, null)
|
|
566
|
-
return@AsyncFunction
|
|
567
242
|
}
|
|
243
|
+
}
|
|
568
244
|
}
|
|
569
245
|
|
|
570
|
-
AsyncFunction("acknowledgePurchaseAndroid") {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
.newBuilder()
|
|
578
|
-
.setPurchaseToken(token)
|
|
579
|
-
.build()
|
|
580
|
-
|
|
581
|
-
billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult: BillingResult ->
|
|
582
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
583
|
-
PlayUtils.rejectPromiseWithBillingError(
|
|
584
|
-
promise,
|
|
585
|
-
billingResult.responseCode,
|
|
586
|
-
)
|
|
587
|
-
return@acknowledgePurchase
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
val map = mutableMapOf<String, Any?>()
|
|
591
|
-
map["responseCode"] = billingResult.responseCode
|
|
592
|
-
map["debugMessage"] = billingResult.debugMessage
|
|
593
|
-
val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
|
|
594
|
-
map["code"] = errorData.code
|
|
595
|
-
map["message"] = errorData.message
|
|
596
|
-
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)
|
|
597
253
|
}
|
|
254
|
+
}
|
|
598
255
|
}
|
|
599
256
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
billingClient.consumeAsync(params) { billingResult: BillingResult, purchaseToken: String? ->
|
|
609
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
610
|
-
PlayUtils.rejectPromiseWithBillingError(
|
|
611
|
-
promise,
|
|
612
|
-
billingResult.responseCode,
|
|
613
|
-
)
|
|
614
|
-
return@consumeAsync
|
|
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)
|
|
615
265
|
}
|
|
616
|
-
|
|
617
|
-
val map = mutableMapOf<String, Any?>()
|
|
618
|
-
map["responseCode"] = billingResult.responseCode
|
|
619
|
-
map["debugMessage"] = billingResult.debugMessage
|
|
620
|
-
val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
|
|
621
|
-
map["code"] = errorData.code
|
|
622
|
-
map["message"] = errorData.message
|
|
623
|
-
map["purchaseTokenAndroid"] = purchaseToken
|
|
624
|
-
promise.resolve(map)
|
|
625
266
|
}
|
|
626
267
|
}
|
|
627
268
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
promise: Promise,
|
|
631
|
-
->
|
|
632
|
-
val billingClient = getBillingClientOrReject(promise) ?: return@AsyncFunction
|
|
633
|
-
billingClient.getBillingConfigAsync(
|
|
634
|
-
GetBillingConfigParams.newBuilder().build(),
|
|
635
|
-
BillingConfigResponseListener { result: BillingResult, config: BillingConfig? ->
|
|
636
|
-
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
637
|
-
promise.safeResolve(config?.countryCode.orEmpty())
|
|
638
|
-
} else {
|
|
639
|
-
val debugMessage = result.debugMessage.orEmpty()
|
|
640
|
-
promise.safeReject(result.responseCode.toString(), debugMessage)
|
|
641
|
-
}
|
|
642
|
-
},
|
|
643
|
-
)
|
|
269
|
+
OnDestroy {
|
|
270
|
+
job.cancel()
|
|
644
271
|
}
|
|
645
272
|
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Rejects promise with billing code if BillingResult is not OK
|
|
649
|
-
*/
|
|
650
|
-
private fun isValidResult(
|
|
651
|
-
billingResult: BillingResult,
|
|
652
|
-
promise: Promise,
|
|
653
|
-
): Boolean {
|
|
654
|
-
Log.d(TAG, "responseCode: " + billingResult.responseCode)
|
|
655
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
656
|
-
PlayUtils.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
657
|
-
return false
|
|
658
|
-
}
|
|
659
|
-
return true
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
private fun getBillingClientOrReject(promise: Promise): BillingClient? {
|
|
663
|
-
val client = billingClientCache
|
|
664
|
-
if (client == null) {
|
|
665
|
-
promise.reject(
|
|
666
|
-
IapErrorCode.E_INIT_CONNECTION,
|
|
667
|
-
"Connection not initialized. Call initConnection() first.",
|
|
668
|
-
null,
|
|
669
|
-
)
|
|
670
|
-
return null
|
|
671
|
-
}
|
|
672
|
-
if (!client.isReady) {
|
|
673
|
-
promise.reject(
|
|
674
|
-
IapErrorCode.E_INIT_CONNECTION,
|
|
675
|
-
"BillingClient not ready. Wait for initConnection() to complete.",
|
|
676
|
-
null,
|
|
677
|
-
)
|
|
678
|
-
return null
|
|
679
|
-
}
|
|
680
|
-
return client
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
private fun initBillingClient(
|
|
684
|
-
promise: Promise,
|
|
685
|
-
callback: (billingClient: BillingClient) -> Unit,
|
|
686
|
-
) {
|
|
687
|
-
if (GoogleApiAvailability
|
|
688
|
-
.getInstance()
|
|
689
|
-
.isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS
|
|
690
|
-
) {
|
|
691
|
-
Log.i(TAG, "Google Play Services are not available on this device")
|
|
692
|
-
promise.reject(
|
|
693
|
-
IapErrorCode.E_NOT_PREPARED,
|
|
694
|
-
"Google Play Services are not available on this device",
|
|
695
|
-
null,
|
|
696
|
-
)
|
|
697
|
-
return
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
billingClientCache =
|
|
701
|
-
BillingClient
|
|
702
|
-
.newBuilder(context)
|
|
703
|
-
.setListener(this)
|
|
704
|
-
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
|
|
705
|
-
.enableAutoServiceReconnection() // Automatically handle service disconnections
|
|
706
|
-
.build()
|
|
707
|
-
|
|
708
|
-
billingClientCache?.startConnection(
|
|
709
|
-
object : BillingClientStateListener {
|
|
710
|
-
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
711
|
-
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
712
|
-
promise.reject(
|
|
713
|
-
IapErrorCode.E_INIT_CONNECTION,
|
|
714
|
-
"Billing setup finished with error: ${billingResult.debugMessage}",
|
|
715
|
-
null,
|
|
716
|
-
)
|
|
717
|
-
return
|
|
718
|
-
}
|
|
719
|
-
callback(billingClientCache!!)
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
override fun onBillingServiceDisconnected() {
|
|
723
|
-
Log.i(TAG, "Billing service disconnected")
|
|
724
|
-
}
|
|
725
|
-
},
|
|
726
|
-
)
|
|
727
|
-
}
|
|
728
273
|
}
|