react-native-iap 8.1.3 → 8.2.2

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.
Binary file
@@ -50,6 +50,9 @@ android {
50
50
  dimension "store"
51
51
  }
52
52
  }
53
+ testOptions {
54
+ unitTests.returnDefaultValues = true
55
+ }
53
56
  }
54
57
 
55
58
  repositories {
@@ -63,6 +66,8 @@ repositories {
63
66
 
64
67
  dependencies {
65
68
  implementation 'com.facebook.react:react-native:+'
69
+ testImplementation 'junit:junit:4.13.1'
70
+ testImplementation "io.mockk:mockk:1.12.4"
66
71
  playImplementation 'com.android.billingclient:billing:4.0.0'
67
72
  def playServicesVersion = safeExtGet('playServicesVersion', DEFAULT_PLAY_SERVICES_VERSION)
68
73
  playImplementation "com.google.android.gms:play-services-base:$playServicesVersion"
@@ -2,6 +2,7 @@ package com.dooboolab.RNIap
2
2
 
3
3
  import com.amazon.device.iap.PurchasingService
4
4
  import com.amazon.device.iap.model.FulfillmentResult
5
+ import com.facebook.react.bridge.LifecycleEventListener
5
6
  import com.facebook.react.bridge.Promise
6
7
  import com.facebook.react.bridge.ReactApplicationContext
7
8
  import com.facebook.react.bridge.ReactContextBaseJavaModule
@@ -10,9 +11,8 @@ import com.facebook.react.bridge.ReadableArray
10
11
  import com.facebook.react.bridge.WritableNativeArray
11
12
  import java.util.HashSet
12
13
 
13
- class RNIapAmazonModule(reactContext: ReactApplicationContext?) :
14
+ class RNIapAmazonModule(reactContext: ReactApplicationContext) :
14
15
  ReactContextBaseJavaModule(reactContext) {
15
- val TAG = "RNIapAmazonModule"
16
16
  override fun getName(): String {
17
17
  return TAG
18
18
  }
@@ -127,5 +127,23 @@ class RNIapAmazonModule(reactContext: ReactApplicationContext?) :
127
127
  const val PROMISE_QUERY_PURCHASES = "PROMISE_QUERY_PURCHASES"
128
128
  const val PROMISE_QUERY_AVAILABLE_ITEMS = "PROMISE_QUERY_AVAILABLE_ITEMS"
129
129
  const val PROMISE_GET_USER_DATA = "PROMISE_GET_USER_DATA"
130
+
131
+ const val TAG = "RNIapAmazonModule"
132
+ }
133
+ init {
134
+ val lifecycleEventListener: LifecycleEventListener = object : LifecycleEventListener {
135
+ /**
136
+ * From https://developer.amazon.com/docs/in-app-purchasing/iap-implement-iap.html#getpurchaseupdates-responses
137
+ * We should fetch updates on resume
138
+ */
139
+ override fun onHostResume() {
140
+ PurchasingService.getUserData()
141
+ PurchasingService.getPurchaseUpdates(false)
142
+ }
143
+ override fun onHostPause() {}
144
+ override fun onHostDestroy() {
145
+ }
146
+ }
147
+ reactContext.addLifecycleEventListener(lifecycleEventListener)
130
148
  }
131
149
  }
@@ -7,7 +7,7 @@ import com.facebook.react.bridge.Promise
7
7
  class PlayUtils {
8
8
  fun rejectPromiseWithBillingError(promise: Promise, responseCode: Int) {
9
9
  val errorData = getBillingResponseData(responseCode)
10
- promise.reject(errorData[0], errorData[1])
10
+ promise.safeReject(errorData[0], errorData[1])
11
11
  }
12
12
 
13
13
  fun getBillingResponseData(responseCode: Int): Array<String?> {
@@ -49,7 +49,7 @@ class PlayUtils {
49
49
  }
50
50
  BillingClient.BillingResponseCode.ERROR -> {
51
51
  errorData[0] = DoobooUtils.E_UNKNOWN
52
- errorData[1] = "An unknown or unexpected error has occured. Please try again later."
52
+ errorData[1] = "An unknown or unexpected error has occurred. Please try again later."
53
53
  }
54
54
  BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
55
55
  errorData[0] = DoobooUtils.E_ALREADY_OWNED
@@ -0,0 +1,33 @@
1
+ package com.dooboolab.RNIap
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.ObjectAlreadyConsumedException
5
+ import com.facebook.react.bridge.Promise
6
+
7
+ /**
8
+ * Extension functions used to simplify promise handling since we don't
9
+ * want to crash in the case of it being resolved/rejected more than once
10
+ */
11
+
12
+ fun Promise.safeResolve(value: Any) {
13
+ try {
14
+ this.resolve(value)
15
+ } catch (oce: ObjectAlreadyConsumedException) {
16
+ Log.d(RNIapModule.TAG, "Already consumed ${oce.message}")
17
+ }
18
+ }
19
+
20
+ fun Promise.safeReject(message: String) = this.safeReject(message, null, null)
21
+
22
+ fun Promise.safeReject(code: String?, message: String?) = this.safeReject(code, message, null)
23
+
24
+ fun Promise.safeReject(code: String?, throwable: Throwable?) =
25
+ this.safeReject(code, null, throwable)
26
+
27
+ fun Promise.safeReject(code: String?, message: String?, throwable: Throwable?) {
28
+ try {
29
+ this.reject(code, message, throwable)
30
+ } catch (oce: ObjectAlreadyConsumedException) {
31
+ Log.d(RNIapModule.TAG, "Already consumed ${oce.message}")
32
+ }
33
+ }
@@ -15,7 +15,6 @@ import com.android.billingclient.api.SkuDetails
15
15
  import com.android.billingclient.api.SkuDetailsParams
16
16
  import com.facebook.react.bridge.Arguments
17
17
  import com.facebook.react.bridge.LifecycleEventListener
18
- import com.facebook.react.bridge.ObjectAlreadyConsumedException
19
18
  import com.facebook.react.bridge.Promise
20
19
  import com.facebook.react.bridge.PromiseImpl
21
20
  import com.facebook.react.bridge.ReactApplicationContext
@@ -32,18 +31,21 @@ import com.google.android.gms.common.GoogleApiAvailability
32
31
  import java.math.BigDecimal
33
32
  import java.util.ArrayList
34
33
 
35
- class RNIapModule(private val reactContext: ReactApplicationContext) :
34
+ class RNIapModule(
35
+ private val reactContext: ReactApplicationContext,
36
+ private val builder: BillingClient.Builder = BillingClient.newBuilder(reactContext).enablePendingPurchases(),
37
+ private val googleApiAvailability: GoogleApiAvailability = GoogleApiAvailability.getInstance()
38
+ ) :
36
39
  ReactContextBaseJavaModule(reactContext),
37
40
  PurchasesUpdatedListener {
38
41
 
39
- private val billingClient: BillingClient = BillingClient.newBuilder(reactContext).enablePendingPurchases().setListener(this)
40
- .build()
42
+ private var billingClient: BillingClient = builder.setListener(this).build()
41
43
  private val skus: MutableMap<String, SkuDetails> = mutableMapOf()
42
44
  override fun getName(): String {
43
- return "RNIapModule"
45
+ return TAG
44
46
  }
45
47
 
46
- private fun ensureConnection(promise: Promise, callback: () -> Unit) {
48
+ internal fun ensureConnection(promise: Promise, callback: () -> Unit) {
47
49
  if (billingClient.isReady) {
48
50
  callback()
49
51
  return
@@ -58,7 +60,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
58
60
  },
59
61
  {
60
62
  if (it.size > 1 && it[0] is String && it[1] is String) {
61
- promise.reject(
63
+ promise.safeReject(
62
64
  it[0] as String, it[1] as String
63
65
  )
64
66
  } else {
@@ -77,31 +79,25 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
77
79
  TAG,
78
80
  "Already initialized, you should only call initConnection() once when your app starts"
79
81
  )
80
- promise.resolve(true)
82
+ promise.safeResolve(true)
81
83
  return
82
84
  }
83
- if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(reactContext)
85
+ if (googleApiAvailability.isGooglePlayServicesAvailable(reactContext)
84
86
  != ConnectionResult.SUCCESS
85
87
  ) {
86
88
  Log.i(TAG, "Google Play Services are not available on this device")
87
- promise.reject(DoobooUtils.E_NOT_PREPARED, "Google Play Services are not available on this device")
89
+ promise.safeReject(DoobooUtils.E_NOT_PREPARED, "Google Play Services are not available on this device")
88
90
  return
89
91
  }
90
92
 
93
+ billingClient = builder.setListener(this).build()
94
+
91
95
  billingClient.startConnection(
92
96
  object : BillingClientStateListener {
93
97
  override fun onBillingSetupFinished(billingResult: BillingResult) {
94
- val responseCode = billingResult.responseCode
95
- try {
96
- if (responseCode == BillingClient.BillingResponseCode.OK) {
97
- promise.resolve(true)
98
- } else {
99
- PlayUtils.instance
100
- .rejectPromiseWithBillingError(promise, responseCode)
101
- }
102
- } catch (oce: ObjectAlreadyConsumedException) {
103
- Log.e(TAG, oce.message!!)
104
- }
98
+ if (!isValidResult(billingResult, promise)) return
99
+
100
+ promise.safeResolve(true)
105
101
  }
106
102
 
107
103
  override fun onBillingServiceDisconnected() {
@@ -113,7 +109,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
113
109
  @ReactMethod
114
110
  fun endConnection(promise: Promise) {
115
111
  billingClient.endConnection()
116
- promise.resolve(true)
112
+ promise.safeResolve(true)
117
113
  }
118
114
 
119
115
  private fun consumeItems(
@@ -138,11 +134,8 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
138
134
  )
139
135
  return@ConsumeResponseListener
140
136
  }
141
- try {
142
- promise.resolve(true)
143
- } catch (oce: ObjectAlreadyConsumedException) {
144
- promise.reject(oce.message)
145
- }
137
+
138
+ promise.safeResolve(true)
146
139
  }
147
140
  billingClient.consumeAsync(consumeParams, listener)
148
141
  }
@@ -154,26 +147,21 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
154
147
  ensureConnection(
155
148
  promise
156
149
  ) {
157
- val array = WritableNativeArray()
158
150
  billingClient.queryPurchasesAsync(
159
151
  BillingClient.SkuType.INAPP
160
- ) { _: BillingResult?, list: List<Purchase>? ->
152
+ ) { billingResult: BillingResult, list: List<Purchase>? ->
153
+ if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
161
154
  if (list == null) {
162
155
  // No purchases found
163
- promise.resolve(false)
156
+ promise.safeResolve(false)
164
157
  return@queryPurchasesAsync
165
158
  }
166
- val pendingPurchases: MutableList<Purchase> = ArrayList()
167
- for (purchase in list) {
168
- // we only want to try to consume PENDING items, in order to force cache-refresh
169
- // for
170
- // them
171
- if (purchase.purchaseState == Purchase.PurchaseState.PENDING) {
172
- pendingPurchases.add(purchase)
173
- }
174
- }
175
- if (pendingPurchases.size == 0) {
176
- promise.resolve(false)
159
+ // we only want to try to consume PENDING items, in order to force cache-refresh
160
+ // for them
161
+ val pendingPurchases = list.filter { it.purchaseState == Purchase.PurchaseState.PENDING }
162
+
163
+ if (pendingPurchases.isEmpty()) {
164
+ promise.safeResolve(false)
177
165
  return@queryPurchasesAsync
178
166
  }
179
167
  consumeItems(
@@ -186,7 +174,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
186
174
  }
187
175
 
188
176
  @ReactMethod
189
- fun getItemsByType(type: String?, skuArr: ReadableArray, promise: Promise) {
177
+ fun getItemsByType(type: String, skuArr: ReadableArray, promise: Promise) {
190
178
  ensureConnection(
191
179
  promise
192
180
  ) {
@@ -198,22 +186,18 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
198
186
  }
199
187
  }
200
188
  val params = SkuDetailsParams.newBuilder()
201
- params.setSkusList(skuList).setType(type!!)
189
+ params.setSkusList(skuList).setType(type)
202
190
  billingClient.querySkuDetailsAsync(
203
191
  params.build()
204
192
  ) { billingResult: BillingResult, skuDetailsList: List<SkuDetails>? ->
205
- Log.d(TAG, "responseCode: " + billingResult.responseCode)
206
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
207
- PlayUtils.instance
208
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
209
- return@querySkuDetailsAsync
210
- }
193
+ if (!isValidResult(billingResult, promise)) return@querySkuDetailsAsync
194
+
211
195
  if (skuDetailsList != null) {
212
196
  for (sku in skuDetailsList) {
213
197
  skus[sku.sku] = sku
214
198
  }
215
199
  }
216
- val items = WritableNativeArray()
200
+ val items = Arguments.createArray()
217
201
  for (skuDetails in skuDetailsList!!) {
218
202
  val item = Arguments.createMap()
219
203
  item.putString("productId", skuDetails.sku)
@@ -263,15 +247,27 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
263
247
  item.putString("originalPrice", originalPrice)
264
248
  items.pushMap(item)
265
249
  }
266
- try {
267
- promise.resolve(items)
268
- } catch (oce: ObjectAlreadyConsumedException) {
269
- Log.e(TAG, oce.message!!)
270
- }
250
+ promise.safeResolve(items)
271
251
  }
272
252
  }
273
253
  }
274
254
 
255
+ /**
256
+ * Rejects promise with billing code if BillingResult is not OK
257
+ */
258
+ private fun isValidResult(
259
+ billingResult: BillingResult,
260
+ promise: Promise
261
+ ): Boolean {
262
+ Log.d(TAG, "responseCode: " + billingResult.responseCode)
263
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
264
+ PlayUtils.instance
265
+ .rejectPromiseWithBillingError(promise, billingResult.responseCode)
266
+ return false
267
+ }
268
+ return true
269
+ }
270
+
275
271
  @ReactMethod
276
272
  fun getAvailableItemsByType(type: String, promise: Promise) {
277
273
  ensureConnection(
@@ -280,7 +276,8 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
280
276
  val items = WritableNativeArray()
281
277
  billingClient.queryPurchasesAsync(
282
278
  if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
283
- ) { billingResult: BillingResult?, purchases: List<Purchase>? ->
279
+ ) { billingResult: BillingResult, purchases: List<Purchase>? ->
280
+ if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
284
281
  if (purchases != null) {
285
282
  for (i in purchases.indices) {
286
283
  val purchase = purchases[i]
@@ -310,11 +307,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
310
307
  items.pushMap(item)
311
308
  }
312
309
  }
313
- try {
314
- promise.resolve(items)
315
- } catch (oce: ObjectAlreadyConsumedException) {
316
- Log.e(TAG, oce.message!!)
317
- }
310
+ promise.safeResolve(items)
318
311
  }
319
312
  }
320
313
  }
@@ -327,16 +320,12 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
327
320
  billingClient.queryPurchaseHistoryAsync(
328
321
  if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
329
322
  ) { billingResult, purchaseHistoryRecordList ->
330
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
331
- PlayUtils.instance
332
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
333
- return@queryPurchaseHistoryAsync
334
- }
323
+ if (!isValidResult(billingResult, promise)) return@queryPurchaseHistoryAsync
324
+
335
325
  Log.d(TAG, purchaseHistoryRecordList.toString())
336
326
  val items = Arguments.createArray()
337
- for (i in purchaseHistoryRecordList!!.indices) {
327
+ purchaseHistoryRecordList?.forEach { purchase ->
338
328
  val item = Arguments.createMap()
339
- val purchase = purchaseHistoryRecordList[i]
340
329
  item.putString("productId", purchase.skus[0])
341
330
  item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
342
331
  item.putString("transactionReceipt", purchase.originalJson)
@@ -346,11 +335,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
346
335
  item.putString("developerPayload", purchase.developerPayload)
347
336
  items.pushMap(item)
348
337
  }
349
- try {
350
- promise.resolve(items)
351
- } catch (oce: ObjectAlreadyConsumedException) {
352
- Log.e(TAG, oce.message!!)
353
- }
338
+ promise.safeResolve(items)
354
339
  }
355
340
  }
356
341
  }
@@ -367,7 +352,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
367
352
  ) {
368
353
  val activity = currentActivity
369
354
  if (activity == null) {
370
- promise.reject(DoobooUtils.E_UNKNOWN, "getCurrentActivity returned null")
355
+ promise.safeReject(DoobooUtils.E_UNKNOWN, "getCurrentActivity returned null")
371
356
  return
372
357
  }
373
358
  ensureConnection(
@@ -387,7 +372,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
387
372
  error.putString("message", debugMessage)
388
373
  error.putString("productId", sku)
389
374
  sendEvent(reactContext, "purchase-error", error)
390
- promise.reject(PROMISE_BUY_ITEM, debugMessage)
375
+ promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
391
376
  return@ensureConnection
392
377
  }
393
378
  builder.setSkuDetails(selectedSku)
@@ -420,7 +405,7 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
420
405
  error.putString("message", debugMessage)
421
406
  error.putString("productId", sku)
422
407
  sendEvent(reactContext, "purchase-error", error)
423
- promise.reject(PROMISE_BUY_ITEM, debugMessage)
408
+ promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
424
409
  return@ensureConnection
425
410
  }
426
411
  } else if (prorationMode
@@ -457,8 +442,14 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
457
442
  }
458
443
  val flowParams = builder.build()
459
444
  val billingResult = billingClient.launchBillingFlow(activity, flowParams)
460
- val errorData: Array<String?> =
461
- PlayUtils.instance.getBillingResponseData(billingResult.responseCode)
445
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
446
+ promise.safeResolve(true)
447
+ return@ensureConnection
448
+ } else {
449
+ val errorData: Array<String?> =
450
+ PlayUtils.instance.getBillingResponseData(billingResult.responseCode)
451
+ promise.safeReject(errorData[0], errorData[1])
452
+ }
462
453
  }
463
454
  }
464
455
 
@@ -478,22 +469,16 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
478
469
  billingClient.acknowledgePurchase(
479
470
  acknowledgePurchaseParams
480
471
  ) { billingResult: BillingResult ->
481
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
482
- PlayUtils.instance
483
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
484
- }
485
- try {
486
- val map = Arguments.createMap()
487
- map.putInt("responseCode", billingResult.responseCode)
488
- map.putString("debugMessage", billingResult.debugMessage)
489
- val errorData: Array<String?> = PlayUtils.instance
490
- .getBillingResponseData(billingResult.responseCode)
491
- map.putString("code", errorData[0])
492
- map.putString("message", errorData[1])
493
- promise.resolve(map)
494
- } catch (oce: ObjectAlreadyConsumedException) {
495
- Log.e(TAG, oce.message!!)
496
- }
472
+ if (!isValidResult(billingResult, promise)) return@acknowledgePurchase
473
+
474
+ val map = Arguments.createMap()
475
+ map.putInt("responseCode", billingResult.responseCode)
476
+ map.putString("debugMessage", billingResult.debugMessage)
477
+ val errorData: Array<String?> = PlayUtils.instance
478
+ .getBillingResponseData(billingResult.responseCode)
479
+ map.putString("code", errorData[0])
480
+ map.putString("message", errorData[1])
481
+ promise.safeResolve(map)
497
482
  }
498
483
  }
499
484
  }
@@ -511,22 +496,16 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
511
496
  billingClient.consumeAsync(
512
497
  params
513
498
  ) { billingResult: BillingResult, purchaseToken: String? ->
514
- if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
515
- PlayUtils.instance
516
- .rejectPromiseWithBillingError(promise, billingResult.responseCode)
517
- }
518
- try {
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.resolve(map)
527
- } catch (oce: ObjectAlreadyConsumedException) {
528
- promise.reject(oce.message)
529
- }
499
+ if (!isValidResult(billingResult, promise)) return@consumeAsync
500
+
501
+ val map = Arguments.createMap()
502
+ map.putInt("responseCode", billingResult.responseCode)
503
+ map.putString("debugMessage", billingResult.debugMessage)
504
+ val errorData: Array<String?> = PlayUtils.instance
505
+ .getBillingResponseData(billingResult.responseCode)
506
+ map.putString("code", errorData[0])
507
+ map.putString("message", errorData[1])
508
+ promise.safeResolve(map)
530
509
  }
531
510
  }
532
511
  }
@@ -602,18 +581,14 @@ class RNIapModule(private val reactContext: ReactApplicationContext) :
602
581
  for (type in types) {
603
582
  billingClient.queryPurchasesAsync(
604
583
  type
605
- ) { billingResult: BillingResult, list: List<Purchase>? ->
606
- val unacknowledgedPurchases = ArrayList<Purchase>()
584
+ ) { billingResult: BillingResult, list: List<Purchase> ->
585
+ if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
607
586
 
608
- for (purchase in list!!) {
609
- if (!purchase.isAcknowledged) {
610
- unacknowledgedPurchases.add(purchase)
611
- }
612
- }
587
+ val unacknowledgedPurchases = list.filter { !it.isAcknowledged }
613
588
  onPurchasesUpdated(billingResult, unacknowledgedPurchases)
614
589
  }
615
590
  }
616
- promise.resolve(true)
591
+ promise.safeResolve(true)
617
592
  }
618
593
  }
619
594
 
@@ -0,0 +1,263 @@
1
+ package com.dooboolab.RNIap
2
+
3
+ import com.android.billingclient.api.BillingClient
4
+ import com.android.billingclient.api.BillingClientStateListener
5
+ import com.android.billingclient.api.BillingResult
6
+ import com.android.billingclient.api.ConsumeResponseListener
7
+ import com.android.billingclient.api.Purchase
8
+ import com.android.billingclient.api.PurchasesResponseListener
9
+ import com.android.billingclient.api.SkuDetailsResponseListener
10
+ import com.facebook.react.bridge.Arguments
11
+ import com.facebook.react.bridge.Promise
12
+ import com.facebook.react.bridge.ReactApplicationContext
13
+ import com.facebook.react.bridge.ReadableArray
14
+ import com.facebook.react.bridge.WritableArray
15
+ import com.facebook.react.bridge.WritableMap
16
+ import com.google.android.gms.common.ConnectionResult
17
+ import com.google.android.gms.common.GoogleApiAvailability
18
+ import io.mockk.MockKAnnotations
19
+ import io.mockk.every
20
+ import io.mockk.impl.annotations.MockK
21
+ import io.mockk.just
22
+ import io.mockk.mockk
23
+ import io.mockk.mockkStatic
24
+ import io.mockk.runs
25
+ import io.mockk.slot
26
+ import io.mockk.verify
27
+ import org.junit.Assert.assertEquals
28
+ import org.junit.Assert.assertTrue
29
+ import org.junit.Before
30
+ import org.junit.Test
31
+
32
+ class RNIapModuleTest {
33
+
34
+ @MockK
35
+ lateinit var context: ReactApplicationContext
36
+
37
+ @MockK
38
+ lateinit var builder: BillingClient.Builder
39
+
40
+ @MockK
41
+ lateinit var billingClient: BillingClient
42
+
43
+ @MockK
44
+ lateinit var availability: GoogleApiAvailability
45
+
46
+ private lateinit var module: RNIapModule
47
+
48
+ @Before
49
+ fun setUp() {
50
+ MockKAnnotations.init(this, relaxUnitFun = true)
51
+ every { builder.setListener(any()) } returns builder
52
+ every { builder.build() } returns billingClient
53
+ module = RNIapModule(context, builder, availability)
54
+ }
55
+
56
+ @Test
57
+ fun `initConnection Already connected should resolve to true`() {
58
+ every { billingClient.isReady } returns true
59
+ val promise = mockk<Promise>(relaxed = true)
60
+
61
+ module.initConnection(promise)
62
+ verify(exactly = 0) { promise.reject(any(), any<String>()) }
63
+ verify { promise.resolve(true) }
64
+ }
65
+
66
+ @Test
67
+ fun `initConnection Play Services not available on device should reject`() {
68
+ every { billingClient.isReady } returns false
69
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.DEVELOPER_ERROR
70
+ val promise = mockk<Promise>(relaxed = true)
71
+
72
+ module.initConnection(promise)
73
+ verify { promise.safeReject(any(), any<String>()) }
74
+ verify(exactly = 0) { promise.resolve(any()) }
75
+ }
76
+
77
+ @Test
78
+ fun `initConnection start new connection succeeds`() {
79
+ every { billingClient.isReady } returns false
80
+ val listener = slot<BillingClientStateListener>()
81
+ every { billingClient.startConnection(capture(listener)) } answers {
82
+ listener.captured.onBillingSetupFinished(
83
+ BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK)
84
+ .build()
85
+ )
86
+ }
87
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
88
+ val promise = mockk<Promise>(relaxed = true)
89
+
90
+ module.initConnection(promise)
91
+ verify(exactly = 0) { promise.reject(any(), any<String>()) }
92
+ verify { promise.resolve(any()) }
93
+ }
94
+
95
+ @Test
96
+ fun `initConnection start new connection fails`() {
97
+ every { billingClient.isReady } returns false
98
+ val listener = slot<BillingClientStateListener>()
99
+ every { billingClient.startConnection(capture(listener)) } answers {
100
+ listener.captured.onBillingSetupFinished(
101
+ BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ERROR)
102
+ .build()
103
+ )
104
+ }
105
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
106
+ val promise = mockk<Promise>(relaxed = true)
107
+
108
+ module.initConnection(promise)
109
+ verify { promise.safeReject(any(), any<String>()) }
110
+ verify(exactly = 0) { promise.resolve(any()) }
111
+ }
112
+
113
+ @Test
114
+ fun `endConnection resolves`() {
115
+ val promise = mockk<Promise>(relaxed = true)
116
+
117
+ module.endConnection(promise)
118
+
119
+ verify { billingClient.endConnection() }
120
+ verify(exactly = 0) { promise.reject(any(), any<String>()) }
121
+ verify { promise.resolve(true) }
122
+ }
123
+
124
+ @Test
125
+ fun `flushFailedPurchasesCachedAsPending resolves to false if no pending purchases`() {
126
+ every { billingClient.isReady } returns true
127
+ val promise = mockk<Promise>(relaxed = true)
128
+ val listener = slot<PurchasesResponseListener>()
129
+ every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers {
130
+ listener.captured.onQueryPurchasesResponse(BillingResult.newBuilder().build(), listOf())
131
+ }
132
+ module.flushFailedPurchasesCachedAsPending(promise)
133
+
134
+ verify(exactly = 0) { promise.reject(any(), any<String>()) }
135
+ verify { promise.resolve(false) } // empty list
136
+ }
137
+
138
+ @Test
139
+ fun `flushFailedPurchasesCachedAsPending resolves to true if pending purchases`() {
140
+ every { billingClient.isReady } returns true
141
+ val promise = mockk<Promise>(relaxed = true)
142
+ val listener = slot<PurchasesResponseListener>()
143
+ every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers {
144
+ listener.captured.onQueryPurchasesResponse(
145
+ BillingResult.newBuilder().build(),
146
+ listOf(
147
+ // 4 = Pending
148
+ mockk<Purchase> {
149
+ every { purchaseState } returns 2
150
+ every { purchaseToken } returns "token"
151
+ },
152
+ Purchase("", "1")
153
+ )
154
+ )
155
+ }
156
+ val consumeListener = slot<ConsumeResponseListener>()
157
+ every { billingClient.consumeAsync(any(), capture(consumeListener)) } answers {
158
+ consumeListener.captured.onConsumeResponse(
159
+ BillingResult.newBuilder()
160
+ .setResponseCode(BillingClient.BillingResponseCode.ITEM_NOT_OWNED).build(),
161
+ ""
162
+ )
163
+ }
164
+
165
+ module.flushFailedPurchasesCachedAsPending(promise)
166
+
167
+ verify(exactly = 0) { promise.reject(any(), any<String>()) }
168
+ verify { promise.resolve(true) } // at least one pending transactions
169
+ }
170
+
171
+ @Test
172
+ fun `ensureConnection should attempt to reconnect, if not in ready state`() {
173
+ every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
174
+ val promise = mockk<Promise>(relaxed = true)
175
+ var isCallbackCalled = false
176
+ val callback = {
177
+ isCallbackCalled = true
178
+ promise.resolve(true)
179
+ }
180
+
181
+ every { billingClient.isReady } returns false andThen true
182
+ module.ensureConnection(promise, callback)
183
+ verify { promise.resolve(true) } // at least one pending transactions
184
+ assertTrue("Should call callback", isCallbackCalled)
185
+ }
186
+
187
+ @Test
188
+ fun getItemsByType() {
189
+ every { billingClient.isReady } returns true
190
+ val promise = mockk<Promise>(relaxed = true)
191
+ val listener = slot<SkuDetailsResponseListener>()
192
+ every { billingClient.querySkuDetailsAsync(any(), capture(listener)) } answers {
193
+ listener.captured.onSkuDetailsResponse(
194
+ BillingResult.newBuilder().build(),
195
+ listOf(
196
+ mockk {
197
+ every { sku } returns "sku1"
198
+ every { introductoryPriceAmountMicros } returns 0
199
+ every { priceAmountMicros } returns 1
200
+ every { priceCurrencyCode } returns "USD"
201
+ every { type } returns "sub"
202
+ every { price } returns "$10.0"
203
+ every { title } returns "My product"
204
+ every { description } returns "My desc"
205
+ every { introductoryPrice } returns "$5.0"
206
+ every { zzc() } returns "com.mypackage"
207
+ every { originalPrice } returns "$13.0"
208
+ every { subscriptionPeriod } returns "3 months"
209
+ every { freeTrialPeriod } returns "1 week"
210
+ every { introductoryPriceCycles } returns 1
211
+ every { introductoryPricePeriod } returns "1"
212
+ every { iconUrl } returns "http://myicon.com/icon"
213
+ every { originalJson } returns "{}"
214
+ every { originalPriceAmountMicros } returns 2
215
+ }
216
+ )
217
+ )
218
+ }
219
+ val skus = mockk<ReadableArray>() {
220
+ every { size() } returns 1
221
+ every { getString(0) } returns "sku0"
222
+ }
223
+ mockkStatic(Arguments::class)
224
+
225
+ val itemsMap = mockk<WritableMap>()
226
+ val itemsArr = mockk<WritableArray>()
227
+ every { Arguments.createMap() } returns itemsMap
228
+ every { Arguments.createArray() } returns itemsArr
229
+ every { itemsMap.putString(any(), any()) } just runs
230
+ var itemsSize = 0
231
+ every { itemsArr.pushMap(any()) } answers {
232
+ itemsSize++
233
+ }
234
+
235
+ module.getItemsByType("subs", skus, promise)
236
+ verify { promise.resolve(any()) }
237
+ assertEquals(itemsSize, 1)
238
+ }
239
+
240
+ @Test
241
+ fun getAvailableItemsByType() {
242
+ }
243
+
244
+ @Test
245
+ fun getPurchaseHistoryByType() {
246
+ }
247
+
248
+ @Test
249
+ fun buyItemByType() {
250
+ }
251
+
252
+ @Test
253
+ fun acknowledgePurchase() {
254
+ }
255
+
256
+ @Test
257
+ fun consumeProduct() {
258
+ }
259
+
260
+ @Test
261
+ fun onPurchasesUpdated() {
262
+ }
263
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-iap",
3
- "version": "8.1.3",
3
+ "version": "8.2.2",
4
4
  "packageManager": "yarn@3.2.0",
5
5
  "description": "React Native In App Purchase Module.",
6
6
  "main": "index.js",