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