react-native-iap 8.1.1 → 8.2.0

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