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.
@@ -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
  }