react-native-iap 8.4.0 → 9.0.0-beta3
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/.yarn/install-state.gz +0 -0
- package/android/build.gradle +3 -3
- package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonListener.kt +16 -4
- package/android/src/play/java/com/dooboolab/RNIap/RNIapModule.kt +143 -98
- package/android/src/play/java/com/dooboolab/RNIap/RNIapModuleInterface.kt +44 -0
- package/android/src/play/java/com/dooboolab/RNIap/RNIapModuleV4.kt +656 -0
- package/android/src/play/java/com/dooboolab/RNIap/RNIapPackage.kt +1 -0
- package/android/src/testPlay/java/com/dooboolab/RNIap/{RNIapModuleTest.kt → RNIapModuleTestV4.kt} +5 -5
- package/ios/RNIapIos.m +1 -0
- package/ios/RNIapIos.swift +6 -8
- package/package.json +1 -1
- package/src/iap.d.ts +6 -4
- package/src/iap.js +44 -32
- package/src/types/index.d.ts +36 -0
- package/ios/RNIapQueue.swift +0 -36
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
package com.dooboolab.RNIap
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.android.billingclient.api.AcknowledgePurchaseParams
|
|
5
|
+
import com.android.billingclient.api.BillingClient
|
|
6
|
+
import com.android.billingclient.api.BillingClientStateListener
|
|
7
|
+
import com.android.billingclient.api.BillingFlowParams
|
|
8
|
+
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams
|
|
9
|
+
import com.android.billingclient.api.BillingResult
|
|
10
|
+
import com.android.billingclient.api.ConsumeParams
|
|
11
|
+
import com.android.billingclient.api.ConsumeResponseListener
|
|
12
|
+
import com.android.billingclient.api.Purchase
|
|
13
|
+
import com.android.billingclient.api.PurchasesUpdatedListener
|
|
14
|
+
import com.android.billingclient.api.SkuDetails
|
|
15
|
+
import com.android.billingclient.api.SkuDetailsParams
|
|
16
|
+
import com.facebook.react.bridge.Arguments
|
|
17
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
18
|
+
import com.facebook.react.bridge.Promise
|
|
19
|
+
import com.facebook.react.bridge.PromiseImpl
|
|
20
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
21
|
+
import com.facebook.react.bridge.ReactContext
|
|
22
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
23
|
+
import com.facebook.react.bridge.ReactMethod
|
|
24
|
+
import com.facebook.react.bridge.ReadableArray
|
|
25
|
+
import com.facebook.react.bridge.ReadableType
|
|
26
|
+
import com.facebook.react.bridge.WritableMap
|
|
27
|
+
import com.facebook.react.bridge.WritableNativeArray
|
|
28
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
29
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
|
|
30
|
+
import com.google.android.gms.common.ConnectionResult
|
|
31
|
+
import com.google.android.gms.common.GoogleApiAvailability
|
|
32
|
+
import java.math.BigDecimal
|
|
33
|
+
import java.util.ArrayList
|
|
34
|
+
|
|
35
|
+
class RNIapModuleV4(
|
|
36
|
+
private val reactContext: ReactApplicationContext,
|
|
37
|
+
private val builder: BillingClient.Builder = BillingClient.newBuilder(reactContext).enablePendingPurchases(),
|
|
38
|
+
private val googleApiAvailability: GoogleApiAvailability = GoogleApiAvailability.getInstance()
|
|
39
|
+
) :
|
|
40
|
+
ReactContextBaseJavaModule(reactContext),
|
|
41
|
+
PurchasesUpdatedListener,
|
|
42
|
+
RNIapModuleInterface {
|
|
43
|
+
|
|
44
|
+
private var billingClientCache: BillingClient? = null
|
|
45
|
+
private val skus: MutableMap<String, SkuDetails> = mutableMapOf()
|
|
46
|
+
override fun getName(): String {
|
|
47
|
+
return TAG
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fun ensureConnection(
|
|
51
|
+
promise: Promise,
|
|
52
|
+
callback: (billingClient: BillingClient) -> Unit
|
|
53
|
+
) {
|
|
54
|
+
val billingClient = billingClientCache
|
|
55
|
+
if (billingClient?.isReady == true) {
|
|
56
|
+
callback(billingClient)
|
|
57
|
+
return
|
|
58
|
+
} else {
|
|
59
|
+
val nested = PromiseImpl(
|
|
60
|
+
{
|
|
61
|
+
if (it.isNotEmpty() && it[0] is Boolean && it[0] as Boolean) {
|
|
62
|
+
val connectedBillingClient = billingClientCache
|
|
63
|
+
if (connectedBillingClient?.isReady == true) {
|
|
64
|
+
callback(connectedBillingClient)
|
|
65
|
+
} else {
|
|
66
|
+
promise.safeReject(DoobooUtils.E_NOT_PREPARED, "Unable to auto-initialize connection")
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
Log.i(TAG, "Incorrect parameter in resolve")
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
if (it.size > 1 && it[0] is String && it[1] is String) {
|
|
74
|
+
promise.safeReject(
|
|
75
|
+
it[0] as String,
|
|
76
|
+
it[1] as String
|
|
77
|
+
)
|
|
78
|
+
} else {
|
|
79
|
+
Log.i(TAG, "Incorrect parameters in reject")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
initConnection(nested)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@ReactMethod
|
|
88
|
+
override fun initConnection(promise: Promise) {
|
|
89
|
+
if (googleApiAvailability.isGooglePlayServicesAvailable(reactContext)
|
|
90
|
+
!= ConnectionResult.SUCCESS
|
|
91
|
+
) {
|
|
92
|
+
Log.i(TAG, "Google Play Services are not available on this device")
|
|
93
|
+
promise.safeReject(DoobooUtils.E_NOT_PREPARED, "Google Play Services are not available on this device")
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (billingClientCache?.isReady == true) {
|
|
98
|
+
Log.i(
|
|
99
|
+
TAG,
|
|
100
|
+
"Already initialized, you should only call initConnection() once when your app starts"
|
|
101
|
+
)
|
|
102
|
+
promise.safeResolve(true)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
builder.setListener(this).build().also {
|
|
106
|
+
billingClientCache = it
|
|
107
|
+
it.startConnection(
|
|
108
|
+
object : BillingClientStateListener {
|
|
109
|
+
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
110
|
+
if (!isValidResult(billingResult, promise)) return
|
|
111
|
+
|
|
112
|
+
promise.safeResolve(true)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override fun onBillingServiceDisconnected() {
|
|
116
|
+
Log.i(TAG, "Billing service disconnected")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@ReactMethod
|
|
124
|
+
override fun endConnection(promise: Promise) {
|
|
125
|
+
billingClientCache?.endConnection()
|
|
126
|
+
billingClientCache = null
|
|
127
|
+
promise.safeResolve(true)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private fun consumeItems(
|
|
131
|
+
purchases: List<Purchase>,
|
|
132
|
+
promise: Promise,
|
|
133
|
+
expectedResponseCode: Int = BillingClient.BillingResponseCode.OK
|
|
134
|
+
) {
|
|
135
|
+
for (purchase in purchases) {
|
|
136
|
+
ensureConnection(
|
|
137
|
+
promise
|
|
138
|
+
) { billingClient ->
|
|
139
|
+
val consumeParams =
|
|
140
|
+
ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
|
|
141
|
+
.build()
|
|
142
|
+
val listener =
|
|
143
|
+
ConsumeResponseListener { billingResult: BillingResult, outToken: String? ->
|
|
144
|
+
if (billingResult.responseCode != expectedResponseCode) {
|
|
145
|
+
PlayUtils.instance
|
|
146
|
+
.rejectPromiseWithBillingError(
|
|
147
|
+
promise,
|
|
148
|
+
billingResult.responseCode
|
|
149
|
+
)
|
|
150
|
+
return@ConsumeResponseListener
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
promise.safeResolve(true)
|
|
154
|
+
}
|
|
155
|
+
billingClient.consumeAsync(consumeParams, listener)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@ReactMethod
|
|
161
|
+
override fun flushFailedPurchasesCachedAsPending(promise: Promise) {
|
|
162
|
+
ensureConnection(
|
|
163
|
+
promise
|
|
164
|
+
) { billingClient ->
|
|
165
|
+
billingClient.queryPurchasesAsync(
|
|
166
|
+
BillingClient.SkuType.INAPP
|
|
167
|
+
) { billingResult: BillingResult, list: List<Purchase>? ->
|
|
168
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
169
|
+
if (list == null) {
|
|
170
|
+
// No purchases found
|
|
171
|
+
promise.safeResolve(false)
|
|
172
|
+
return@queryPurchasesAsync
|
|
173
|
+
}
|
|
174
|
+
// we only want to try to consume PENDING items, in order to force cache-refresh
|
|
175
|
+
// for them
|
|
176
|
+
val pendingPurchases = list.filter { it.purchaseState == Purchase.PurchaseState.PENDING }
|
|
177
|
+
|
|
178
|
+
if (pendingPurchases.isEmpty()) {
|
|
179
|
+
promise.safeResolve(false)
|
|
180
|
+
return@queryPurchasesAsync
|
|
181
|
+
}
|
|
182
|
+
consumeItems(
|
|
183
|
+
pendingPurchases,
|
|
184
|
+
promise,
|
|
185
|
+
BillingClient.BillingResponseCode.ITEM_NOT_OWNED
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@ReactMethod
|
|
192
|
+
override fun getItemsByType(type: String, skuArr: ReadableArray, promise: Promise) {
|
|
193
|
+
ensureConnection(
|
|
194
|
+
promise
|
|
195
|
+
) { billingClient ->
|
|
196
|
+
val skuList = ArrayList<String>()
|
|
197
|
+
for (i in 0 until skuArr.size()) {
|
|
198
|
+
if (skuArr.getType(i) == ReadableType.String) {
|
|
199
|
+
val sku: String = skuArr.getString(i)
|
|
200
|
+
skuList.add(sku)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
val params = SkuDetailsParams.newBuilder()
|
|
204
|
+
params.setSkusList(skuList).setType(type)
|
|
205
|
+
billingClient.querySkuDetailsAsync(
|
|
206
|
+
params.build()
|
|
207
|
+
) { billingResult: BillingResult, skuDetailsList: List<SkuDetails>? ->
|
|
208
|
+
if (!isValidResult(billingResult, promise)) return@querySkuDetailsAsync
|
|
209
|
+
|
|
210
|
+
val items = Arguments.createArray()
|
|
211
|
+
if (skuDetailsList != null) {
|
|
212
|
+
for (skuDetails in skuDetailsList) {
|
|
213
|
+
skus[skuDetails.sku] = skuDetails
|
|
214
|
+
|
|
215
|
+
val item = Arguments.createMap()
|
|
216
|
+
item.putString("productId", skuDetails.sku)
|
|
217
|
+
val introductoryPriceMicros = skuDetails.introductoryPriceAmountMicros
|
|
218
|
+
val priceAmountMicros = skuDetails.priceAmountMicros
|
|
219
|
+
// Use valueOf instead of constructors.
|
|
220
|
+
// See:
|
|
221
|
+
// https://www.javaworld.com/article/2073176/caution--double-to-bigdecimal-in-java.html
|
|
222
|
+
val priceAmount = BigDecimal.valueOf(priceAmountMicros)
|
|
223
|
+
val introductoryPriceAmount =
|
|
224
|
+
BigDecimal.valueOf(introductoryPriceMicros)
|
|
225
|
+
val microUnitsDivisor = BigDecimal.valueOf(1000000)
|
|
226
|
+
val price = priceAmount.divide(microUnitsDivisor).toString()
|
|
227
|
+
val introductoryPriceAsAmountAndroid =
|
|
228
|
+
introductoryPriceAmount.divide(microUnitsDivisor).toString()
|
|
229
|
+
item.putString("price", price)
|
|
230
|
+
item.putString("currency", skuDetails.priceCurrencyCode)
|
|
231
|
+
item.putString("type", skuDetails.type)
|
|
232
|
+
item.putString("localizedPrice", skuDetails.price)
|
|
233
|
+
item.putString("title", skuDetails.title)
|
|
234
|
+
item.putString("description", skuDetails.description)
|
|
235
|
+
item.putString("introductoryPrice", skuDetails.introductoryPrice)
|
|
236
|
+
item.putString("typeAndroid", skuDetails.type)
|
|
237
|
+
item.putString("packageNameAndroid", skuDetails.zzc())
|
|
238
|
+
item.putString("originalPriceAndroid", skuDetails.originalPrice)
|
|
239
|
+
item.putString(
|
|
240
|
+
"subscriptionPeriodAndroid",
|
|
241
|
+
skuDetails.subscriptionPeriod
|
|
242
|
+
)
|
|
243
|
+
item.putString("freeTrialPeriodAndroid", skuDetails.freeTrialPeriod)
|
|
244
|
+
item.putString(
|
|
245
|
+
"introductoryPriceCyclesAndroid",
|
|
246
|
+
skuDetails.introductoryPriceCycles.toString()
|
|
247
|
+
)
|
|
248
|
+
item.putString(
|
|
249
|
+
"introductoryPricePeriodAndroid",
|
|
250
|
+
skuDetails.introductoryPricePeriod
|
|
251
|
+
)
|
|
252
|
+
item.putString(
|
|
253
|
+
"introductoryPriceAsAmountAndroid",
|
|
254
|
+
introductoryPriceAsAmountAndroid
|
|
255
|
+
)
|
|
256
|
+
item.putString("iconUrl", skuDetails.iconUrl)
|
|
257
|
+
item.putString("originalJson", skuDetails.originalJson)
|
|
258
|
+
val originalPriceAmountMicros =
|
|
259
|
+
BigDecimal.valueOf(skuDetails.originalPriceAmountMicros)
|
|
260
|
+
val originalPrice =
|
|
261
|
+
originalPriceAmountMicros.divide(microUnitsDivisor).toString()
|
|
262
|
+
item.putString("originalPrice", originalPrice)
|
|
263
|
+
items.pushMap(item)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
promise.safeResolve(items)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Rejects promise with billing code if BillingResult is not OK
|
|
273
|
+
*/
|
|
274
|
+
private fun isValidResult(
|
|
275
|
+
billingResult: BillingResult,
|
|
276
|
+
promise: Promise
|
|
277
|
+
): Boolean {
|
|
278
|
+
Log.d(TAG, "responseCode: " + billingResult.responseCode)
|
|
279
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
280
|
+
PlayUtils.instance
|
|
281
|
+
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
return true
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
@ReactMethod
|
|
288
|
+
override fun getAvailableItemsByType(type: String, promise: Promise) {
|
|
289
|
+
ensureConnection(
|
|
290
|
+
promise
|
|
291
|
+
) { billingClient ->
|
|
292
|
+
val items = WritableNativeArray()
|
|
293
|
+
billingClient.queryPurchasesAsync(
|
|
294
|
+
if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
|
|
295
|
+
) { billingResult: BillingResult, purchases: List<Purchase>? ->
|
|
296
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
297
|
+
if (purchases != null) {
|
|
298
|
+
for (i in purchases.indices) {
|
|
299
|
+
val purchase = purchases[i]
|
|
300
|
+
val item = WritableNativeMap()
|
|
301
|
+
item.putString("productId", purchase.skus[0])
|
|
302
|
+
item.putString("transactionId", purchase.orderId)
|
|
303
|
+
item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
|
|
304
|
+
item.putString("transactionReceipt", purchase.originalJson)
|
|
305
|
+
item.putString("orderId", purchase.orderId)
|
|
306
|
+
item.putString("purchaseToken", purchase.purchaseToken)
|
|
307
|
+
item.putString("developerPayloadAndroid", purchase.developerPayload)
|
|
308
|
+
item.putString("signatureAndroid", purchase.signature)
|
|
309
|
+
item.putInt("purchaseStateAndroid", purchase.purchaseState)
|
|
310
|
+
item.putBoolean("isAcknowledgedAndroid", purchase.isAcknowledged)
|
|
311
|
+
item.putString("packageNameAndroid", purchase.packageName)
|
|
312
|
+
item.putString(
|
|
313
|
+
"obfuscatedAccountIdAndroid",
|
|
314
|
+
purchase.accountIdentifiers?.obfuscatedAccountId
|
|
315
|
+
)
|
|
316
|
+
item.putString(
|
|
317
|
+
"obfuscatedProfileIdAndroid",
|
|
318
|
+
purchase.accountIdentifiers?.obfuscatedProfileId
|
|
319
|
+
)
|
|
320
|
+
if (type == BillingClient.SkuType.SUBS) {
|
|
321
|
+
item.putBoolean("autoRenewingAndroid", purchase.isAutoRenewing)
|
|
322
|
+
}
|
|
323
|
+
items.pushMap(item)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
promise.safeResolve(items)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@ReactMethod
|
|
332
|
+
override fun getPurchaseHistoryByType(type: String, promise: Promise) {
|
|
333
|
+
ensureConnection(
|
|
334
|
+
promise
|
|
335
|
+
) { billingClient ->
|
|
336
|
+
billingClient.queryPurchaseHistoryAsync(
|
|
337
|
+
if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
|
|
338
|
+
) { billingResult, purchaseHistoryRecordList ->
|
|
339
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchaseHistoryAsync
|
|
340
|
+
|
|
341
|
+
Log.d(TAG, purchaseHistoryRecordList.toString())
|
|
342
|
+
val items = Arguments.createArray()
|
|
343
|
+
purchaseHistoryRecordList?.forEach { purchase ->
|
|
344
|
+
val item = Arguments.createMap()
|
|
345
|
+
item.putString("productId", purchase.skus[0])
|
|
346
|
+
item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
|
|
347
|
+
item.putString("transactionReceipt", purchase.originalJson)
|
|
348
|
+
item.putString("purchaseToken", purchase.purchaseToken)
|
|
349
|
+
item.putString("dataAndroid", purchase.originalJson)
|
|
350
|
+
item.putString("signatureAndroid", purchase.signature)
|
|
351
|
+
item.putString("developerPayload", purchase.developerPayload)
|
|
352
|
+
items.pushMap(item)
|
|
353
|
+
}
|
|
354
|
+
promise.safeResolve(items)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@ReactMethod
|
|
360
|
+
override fun buyItemByType(
|
|
361
|
+
type: String,
|
|
362
|
+
sku: String,
|
|
363
|
+
purchaseToken: String?,
|
|
364
|
+
prorationMode: Int?,
|
|
365
|
+
obfuscatedAccountId: String?,
|
|
366
|
+
obfuscatedProfileId: String?,
|
|
367
|
+
selectedOfferIndex: Int?, // New optional parameter in V5 (added to maintain interface consistency)
|
|
368
|
+
promise: Promise
|
|
369
|
+
) {
|
|
370
|
+
val activity = currentActivity
|
|
371
|
+
if (activity == null) {
|
|
372
|
+
promise.safeReject(DoobooUtils.E_UNKNOWN, "getCurrentActivity returned null")
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
ensureConnection(
|
|
376
|
+
promise
|
|
377
|
+
) { billingClient ->
|
|
378
|
+
DoobooUtils.instance.addPromiseForKey(
|
|
379
|
+
PROMISE_BUY_ITEM,
|
|
380
|
+
promise
|
|
381
|
+
)
|
|
382
|
+
val builder = BillingFlowParams.newBuilder()
|
|
383
|
+
val selectedSku: SkuDetails? = skus[sku]
|
|
384
|
+
if (selectedSku == null) {
|
|
385
|
+
val debugMessage =
|
|
386
|
+
"The sku was not found. Please fetch products first by calling getItems"
|
|
387
|
+
val error = Arguments.createMap()
|
|
388
|
+
error.putString("debugMessage", debugMessage)
|
|
389
|
+
error.putString("code", PROMISE_BUY_ITEM)
|
|
390
|
+
error.putString("message", debugMessage)
|
|
391
|
+
error.putString("productId", sku)
|
|
392
|
+
sendEvent(reactContext, "purchase-error", error)
|
|
393
|
+
promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
|
|
394
|
+
return@ensureConnection
|
|
395
|
+
}
|
|
396
|
+
builder.setSkuDetails(selectedSku)
|
|
397
|
+
val subscriptionUpdateParamsBuilder = SubscriptionUpdateParams.newBuilder()
|
|
398
|
+
if (purchaseToken != null) {
|
|
399
|
+
subscriptionUpdateParamsBuilder.setOldSkuPurchaseToken(purchaseToken)
|
|
400
|
+
}
|
|
401
|
+
if (obfuscatedAccountId != null) {
|
|
402
|
+
builder.setObfuscatedAccountId(obfuscatedAccountId)
|
|
403
|
+
}
|
|
404
|
+
if (obfuscatedProfileId != null) {
|
|
405
|
+
builder.setObfuscatedProfileId(obfuscatedProfileId)
|
|
406
|
+
}
|
|
407
|
+
if (prorationMode != null && prorationMode != -1) {
|
|
408
|
+
if (prorationMode
|
|
409
|
+
== BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE
|
|
410
|
+
) {
|
|
411
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
412
|
+
BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE
|
|
413
|
+
)
|
|
414
|
+
if (type != BillingClient.SkuType.SUBS) {
|
|
415
|
+
val debugMessage =
|
|
416
|
+
(
|
|
417
|
+
"IMMEDIATE_AND_CHARGE_PRORATED_PRICE for proration mode only works in" +
|
|
418
|
+
" subscription purchase."
|
|
419
|
+
)
|
|
420
|
+
val error = Arguments.createMap()
|
|
421
|
+
error.putString("debugMessage", debugMessage)
|
|
422
|
+
error.putString("code", PROMISE_BUY_ITEM)
|
|
423
|
+
error.putString("message", debugMessage)
|
|
424
|
+
error.putString("productId", sku)
|
|
425
|
+
sendEvent(reactContext, "purchase-error", error)
|
|
426
|
+
promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
|
|
427
|
+
return@ensureConnection
|
|
428
|
+
}
|
|
429
|
+
} else if (prorationMode
|
|
430
|
+
== BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION
|
|
431
|
+
) {
|
|
432
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
433
|
+
BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION
|
|
434
|
+
)
|
|
435
|
+
} else if (prorationMode == BillingFlowParams.ProrationMode.DEFERRED) {
|
|
436
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
437
|
+
BillingFlowParams.ProrationMode.DEFERRED
|
|
438
|
+
)
|
|
439
|
+
} else if (prorationMode
|
|
440
|
+
== BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION
|
|
441
|
+
) {
|
|
442
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
443
|
+
BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION
|
|
444
|
+
)
|
|
445
|
+
} else if (prorationMode
|
|
446
|
+
== BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
|
|
447
|
+
) {
|
|
448
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
449
|
+
BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
|
|
450
|
+
)
|
|
451
|
+
} else {
|
|
452
|
+
subscriptionUpdateParamsBuilder.setReplaceSkusProrationMode(
|
|
453
|
+
BillingFlowParams.ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (purchaseToken != null) {
|
|
458
|
+
val subscriptionUpdateParams = subscriptionUpdateParamsBuilder.build()
|
|
459
|
+
builder.setSubscriptionUpdateParams(subscriptionUpdateParams)
|
|
460
|
+
}
|
|
461
|
+
val flowParams = builder.build()
|
|
462
|
+
val billingResultCode = billingClient.launchBillingFlow(activity, flowParams)?.responseCode ?: BillingClient.BillingResponseCode.ERROR
|
|
463
|
+
if (billingResultCode == BillingClient.BillingResponseCode.OK) {
|
|
464
|
+
promise.safeResolve(true)
|
|
465
|
+
return@ensureConnection
|
|
466
|
+
} else {
|
|
467
|
+
val errorData: Array<String?> =
|
|
468
|
+
PlayUtils.instance.getBillingResponseData(billingResultCode)
|
|
469
|
+
promise.safeReject(errorData[0], errorData[1])
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
@ReactMethod
|
|
475
|
+
override fun acknowledgePurchase(
|
|
476
|
+
token: String,
|
|
477
|
+
developerPayLoad: String?,
|
|
478
|
+
promise: Promise
|
|
479
|
+
) {
|
|
480
|
+
ensureConnection(
|
|
481
|
+
promise
|
|
482
|
+
) { billingClient ->
|
|
483
|
+
val acknowledgePurchaseParams =
|
|
484
|
+
AcknowledgePurchaseParams.newBuilder().setPurchaseToken(
|
|
485
|
+
token
|
|
486
|
+
).build()
|
|
487
|
+
billingClient.acknowledgePurchase(
|
|
488
|
+
acknowledgePurchaseParams
|
|
489
|
+
) { billingResult: BillingResult ->
|
|
490
|
+
if (!isValidResult(billingResult, promise)) return@acknowledgePurchase
|
|
491
|
+
|
|
492
|
+
val map = Arguments.createMap()
|
|
493
|
+
map.putInt("responseCode", billingResult.responseCode)
|
|
494
|
+
map.putString("debugMessage", billingResult.debugMessage)
|
|
495
|
+
val errorData: Array<String?> = PlayUtils.instance
|
|
496
|
+
.getBillingResponseData(billingResult.responseCode)
|
|
497
|
+
map.putString("code", errorData[0])
|
|
498
|
+
map.putString("message", errorData[1])
|
|
499
|
+
promise.safeResolve(map)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
@ReactMethod
|
|
505
|
+
override fun consumeProduct(
|
|
506
|
+
token: String,
|
|
507
|
+
developerPayLoad: String?,
|
|
508
|
+
promise: Promise
|
|
509
|
+
) {
|
|
510
|
+
val params = ConsumeParams.newBuilder().setPurchaseToken(token).build()
|
|
511
|
+
ensureConnection(
|
|
512
|
+
promise
|
|
513
|
+
) { billingClient ->
|
|
514
|
+
billingClient.consumeAsync(
|
|
515
|
+
params
|
|
516
|
+
) { billingResult: BillingResult, purchaseToken: String? ->
|
|
517
|
+
if (!isValidResult(billingResult, promise)) return@consumeAsync
|
|
518
|
+
|
|
519
|
+
val map = Arguments.createMap()
|
|
520
|
+
map.putInt("responseCode", billingResult.responseCode)
|
|
521
|
+
map.putString("debugMessage", billingResult.debugMessage)
|
|
522
|
+
val errorData: Array<String?> = PlayUtils.instance
|
|
523
|
+
.getBillingResponseData(billingResult.responseCode)
|
|
524
|
+
map.putString("code", errorData[0])
|
|
525
|
+
map.putString("message", errorData[1])
|
|
526
|
+
promise.safeResolve(map)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
|
|
532
|
+
val responseCode = billingResult.responseCode
|
|
533
|
+
if (responseCode != BillingClient.BillingResponseCode.OK) {
|
|
534
|
+
val error = Arguments.createMap()
|
|
535
|
+
error.putInt("responseCode", responseCode)
|
|
536
|
+
error.putString("debugMessage", billingResult.debugMessage)
|
|
537
|
+
val errorData: Array<String?> =
|
|
538
|
+
PlayUtils.instance.getBillingResponseData(responseCode)
|
|
539
|
+
error.putString("code", errorData[0])
|
|
540
|
+
error.putString("message", errorData[1])
|
|
541
|
+
sendEvent(reactContext, "purchase-error", error)
|
|
542
|
+
PlayUtils.instance.rejectPromisesWithBillingError(PROMISE_BUY_ITEM, responseCode)
|
|
543
|
+
return
|
|
544
|
+
}
|
|
545
|
+
if (purchases != null) {
|
|
546
|
+
var promiseItem: WritableMap? = null
|
|
547
|
+
for (i in purchases.indices) {
|
|
548
|
+
val item = Arguments.createMap()
|
|
549
|
+
val purchase = purchases[i]
|
|
550
|
+
item.putString("productId", purchase.skus[0])
|
|
551
|
+
item.putString("transactionId", purchase.orderId)
|
|
552
|
+
item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
|
|
553
|
+
item.putString("transactionReceipt", purchase.originalJson)
|
|
554
|
+
item.putString("purchaseToken", purchase.purchaseToken)
|
|
555
|
+
item.putString("dataAndroid", purchase.originalJson)
|
|
556
|
+
item.putString("signatureAndroid", purchase.signature)
|
|
557
|
+
item.putBoolean("autoRenewingAndroid", purchase.isAutoRenewing)
|
|
558
|
+
item.putBoolean("isAcknowledgedAndroid", purchase.isAcknowledged)
|
|
559
|
+
item.putInt("purchaseStateAndroid", purchase.purchaseState)
|
|
560
|
+
item.putString("packageNameAndroid", purchase.packageName)
|
|
561
|
+
item.putString("developerPayloadAndroid", purchase.developerPayload)
|
|
562
|
+
val accountIdentifiers = purchase.accountIdentifiers
|
|
563
|
+
if (accountIdentifiers != null) {
|
|
564
|
+
item.putString(
|
|
565
|
+
"obfuscatedAccountIdAndroid",
|
|
566
|
+
accountIdentifiers.obfuscatedAccountId
|
|
567
|
+
)
|
|
568
|
+
item.putString(
|
|
569
|
+
"obfuscatedProfileIdAndroid",
|
|
570
|
+
accountIdentifiers.obfuscatedProfileId
|
|
571
|
+
)
|
|
572
|
+
}
|
|
573
|
+
promiseItem = WritableNativeMap()
|
|
574
|
+
promiseItem.merge(item)
|
|
575
|
+
sendEvent(reactContext, "purchase-updated", item)
|
|
576
|
+
}
|
|
577
|
+
if (promiseItem != null) {
|
|
578
|
+
DoobooUtils.instance.resolvePromisesForKey(PROMISE_BUY_ITEM, promiseItem)
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
val result = Arguments.createMap()
|
|
582
|
+
result.putInt("responseCode", billingResult.responseCode)
|
|
583
|
+
result.putString("debugMessage", billingResult.debugMessage)
|
|
584
|
+
result.putString(
|
|
585
|
+
"extraMessage",
|
|
586
|
+
"The purchases are null. This is a normal behavior if you have requested DEFERRED" +
|
|
587
|
+
" proration. If not please report an issue."
|
|
588
|
+
)
|
|
589
|
+
sendEvent(reactContext, "purchase-updated", result)
|
|
590
|
+
DoobooUtils.instance.resolvePromisesForKey(PROMISE_BUY_ITEM, null)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private fun sendUnconsumedPurchases(promise: Promise) {
|
|
595
|
+
ensureConnection(
|
|
596
|
+
promise
|
|
597
|
+
) { billingClient ->
|
|
598
|
+
val types = arrayOf(BillingClient.SkuType.INAPP, BillingClient.SkuType.SUBS)
|
|
599
|
+
for (type in types) {
|
|
600
|
+
billingClient.queryPurchasesAsync(
|
|
601
|
+
type
|
|
602
|
+
) { billingResult: BillingResult, list: List<Purchase> ->
|
|
603
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
604
|
+
|
|
605
|
+
val unacknowledgedPurchases = list.filter { !it.isAcknowledged }
|
|
606
|
+
onPurchasesUpdated(billingResult, unacknowledgedPurchases)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
promise.safeResolve(true)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
@ReactMethod
|
|
614
|
+
override fun startListening(promise: Promise) {
|
|
615
|
+
sendUnconsumedPurchases(promise)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
@ReactMethod
|
|
619
|
+
override fun addListener(eventName: String) {
|
|
620
|
+
// Keep: Required for RN built-in Event Emitter Calls.
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
@ReactMethod
|
|
624
|
+
override fun removeListeners(count: Double) {
|
|
625
|
+
// Keep: Required for RN built-in Event Emitter Calls.
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
@ReactMethod
|
|
629
|
+
override fun getPackageName(promise: Promise) = promise.resolve(reactApplicationContext.packageName)
|
|
630
|
+
|
|
631
|
+
private fun sendEvent(
|
|
632
|
+
reactContext: ReactContext,
|
|
633
|
+
eventName: String,
|
|
634
|
+
params: WritableMap?
|
|
635
|
+
) {
|
|
636
|
+
reactContext
|
|
637
|
+
.getJSModule(RCTDeviceEventEmitter::class.java)
|
|
638
|
+
.emit(eventName, params)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
companion object {
|
|
642
|
+
private const val PROMISE_BUY_ITEM = "PROMISE_BUY_ITEM"
|
|
643
|
+
const val TAG = "RNIapModuleV4"
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
init {
|
|
647
|
+
val lifecycleEventListener: LifecycleEventListener = object : LifecycleEventListener {
|
|
648
|
+
override fun onHostResume() {}
|
|
649
|
+
override fun onHostPause() {}
|
|
650
|
+
override fun onHostDestroy() {
|
|
651
|
+
billingClientCache?.endConnection()
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
reactContext.addLifecycleEventListener(lifecycleEventListener)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
@@ -15,6 +15,7 @@ class RNIapPackage : ReactPackage {
|
|
|
15
15
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
16
16
|
val modules: MutableList<NativeModule> = ArrayList()
|
|
17
17
|
modules.add(RNIapModule(reactContext))
|
|
18
|
+
modules.add(RNIapModuleV4(reactContext))
|
|
18
19
|
return modules
|
|
19
20
|
}
|
|
20
21
|
}
|