react-native-iap 8.2.1 → 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.
- package/.yarn/install-state.gz +0 -0
- package/android/build.gradle +1 -1
- package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonModule.kt +20 -2
- package/android/src/play/java/com/dooboolab/RNIap/RNIapModule.kt +45 -43
- package/android/src/testPlay/java/com/dooboolab/RNIap/RNIapModuleTest.kt +74 -3
- package/package.json +1 -1
package/.yarn/install-state.gz
CHANGED
|
Binary file
|
package/android/build.gradle
CHANGED
|
@@ -66,7 +66,7 @@ repositories {
|
|
|
66
66
|
|
|
67
67
|
dependencies {
|
|
68
68
|
implementation 'com.facebook.react:react-native:+'
|
|
69
|
-
testImplementation 'junit:junit:4.
|
|
69
|
+
testImplementation 'junit:junit:4.13.1'
|
|
70
70
|
testImplementation "io.mockk:mockk:1.12.4"
|
|
71
71
|
playImplementation 'com.android.billingclient:billing:4.0.0'
|
|
72
72
|
def playServicesVersion = safeExtGet('playServicesVersion', DEFAULT_PLAY_SERVICES_VERSION)
|
|
@@ -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
|
}
|
|
@@ -42,7 +42,7 @@ class RNIapModule(
|
|
|
42
42
|
private var billingClient: BillingClient = builder.setListener(this).build()
|
|
43
43
|
private val skus: MutableMap<String, SkuDetails> = mutableMapOf()
|
|
44
44
|
override fun getName(): String {
|
|
45
|
-
return
|
|
45
|
+
return TAG
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
internal fun ensureConnection(promise: Promise, callback: () -> Unit) {
|
|
@@ -95,14 +95,9 @@ class RNIapModule(
|
|
|
95
95
|
billingClient.startConnection(
|
|
96
96
|
object : BillingClientStateListener {
|
|
97
97
|
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
98
|
-
|
|
98
|
+
if (!isValidResult(billingResult, promise)) return
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
promise.safeResolve(true)
|
|
102
|
-
} else {
|
|
103
|
-
PlayUtils.instance
|
|
104
|
-
.rejectPromiseWithBillingError(promise, responseCode)
|
|
105
|
-
}
|
|
100
|
+
promise.safeResolve(true)
|
|
106
101
|
}
|
|
107
102
|
|
|
108
103
|
override fun onBillingServiceDisconnected() {
|
|
@@ -154,7 +149,8 @@ class RNIapModule(
|
|
|
154
149
|
) {
|
|
155
150
|
billingClient.queryPurchasesAsync(
|
|
156
151
|
BillingClient.SkuType.INAPP
|
|
157
|
-
) {
|
|
152
|
+
) { billingResult: BillingResult, list: List<Purchase>? ->
|
|
153
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
158
154
|
if (list == null) {
|
|
159
155
|
// No purchases found
|
|
160
156
|
promise.safeResolve(false)
|
|
@@ -178,7 +174,7 @@ class RNIapModule(
|
|
|
178
174
|
}
|
|
179
175
|
|
|
180
176
|
@ReactMethod
|
|
181
|
-
fun getItemsByType(type: String
|
|
177
|
+
fun getItemsByType(type: String, skuArr: ReadableArray, promise: Promise) {
|
|
182
178
|
ensureConnection(
|
|
183
179
|
promise
|
|
184
180
|
) {
|
|
@@ -190,22 +186,18 @@ class RNIapModule(
|
|
|
190
186
|
}
|
|
191
187
|
}
|
|
192
188
|
val params = SkuDetailsParams.newBuilder()
|
|
193
|
-
params.setSkusList(skuList).setType(type
|
|
189
|
+
params.setSkusList(skuList).setType(type)
|
|
194
190
|
billingClient.querySkuDetailsAsync(
|
|
195
191
|
params.build()
|
|
196
192
|
) { billingResult: BillingResult, skuDetailsList: List<SkuDetails>? ->
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
PlayUtils.instance
|
|
200
|
-
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
201
|
-
return@querySkuDetailsAsync
|
|
202
|
-
}
|
|
193
|
+
if (!isValidResult(billingResult, promise)) return@querySkuDetailsAsync
|
|
194
|
+
|
|
203
195
|
if (skuDetailsList != null) {
|
|
204
196
|
for (sku in skuDetailsList) {
|
|
205
197
|
skus[sku.sku] = sku
|
|
206
198
|
}
|
|
207
199
|
}
|
|
208
|
-
val items =
|
|
200
|
+
val items = Arguments.createArray()
|
|
209
201
|
for (skuDetails in skuDetailsList!!) {
|
|
210
202
|
val item = Arguments.createMap()
|
|
211
203
|
item.putString("productId", skuDetails.sku)
|
|
@@ -260,6 +252,22 @@ class RNIapModule(
|
|
|
260
252
|
}
|
|
261
253
|
}
|
|
262
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
|
+
|
|
263
271
|
@ReactMethod
|
|
264
272
|
fun getAvailableItemsByType(type: String, promise: Promise) {
|
|
265
273
|
ensureConnection(
|
|
@@ -268,7 +276,8 @@ class RNIapModule(
|
|
|
268
276
|
val items = WritableNativeArray()
|
|
269
277
|
billingClient.queryPurchasesAsync(
|
|
270
278
|
if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
|
|
271
|
-
) { billingResult: BillingResult
|
|
279
|
+
) { billingResult: BillingResult, purchases: List<Purchase>? ->
|
|
280
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
272
281
|
if (purchases != null) {
|
|
273
282
|
for (i in purchases.indices) {
|
|
274
283
|
val purchase = purchases[i]
|
|
@@ -311,16 +320,12 @@ class RNIapModule(
|
|
|
311
320
|
billingClient.queryPurchaseHistoryAsync(
|
|
312
321
|
if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
|
|
313
322
|
) { billingResult, purchaseHistoryRecordList ->
|
|
314
|
-
if (billingResult
|
|
315
|
-
|
|
316
|
-
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
317
|
-
return@queryPurchaseHistoryAsync
|
|
318
|
-
}
|
|
323
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchaseHistoryAsync
|
|
324
|
+
|
|
319
325
|
Log.d(TAG, purchaseHistoryRecordList.toString())
|
|
320
326
|
val items = Arguments.createArray()
|
|
321
|
-
|
|
327
|
+
purchaseHistoryRecordList?.forEach { purchase ->
|
|
322
328
|
val item = Arguments.createMap()
|
|
323
|
-
val purchase = purchaseHistoryRecordList[i]
|
|
324
329
|
item.putString("productId", purchase.skus[0])
|
|
325
330
|
item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
|
|
326
331
|
item.putString("transactionReceipt", purchase.originalJson)
|
|
@@ -437,8 +442,14 @@ class RNIapModule(
|
|
|
437
442
|
}
|
|
438
443
|
val flowParams = builder.build()
|
|
439
444
|
val billingResult = billingClient.launchBillingFlow(activity, flowParams)
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
}
|
|
442
453
|
}
|
|
443
454
|
}
|
|
444
455
|
|
|
@@ -458,10 +469,8 @@ class RNIapModule(
|
|
|
458
469
|
billingClient.acknowledgePurchase(
|
|
459
470
|
acknowledgePurchaseParams
|
|
460
471
|
) { billingResult: BillingResult ->
|
|
461
|
-
if (billingResult
|
|
462
|
-
|
|
463
|
-
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
464
|
-
}
|
|
472
|
+
if (!isValidResult(billingResult, promise)) return@acknowledgePurchase
|
|
473
|
+
|
|
465
474
|
val map = Arguments.createMap()
|
|
466
475
|
map.putInt("responseCode", billingResult.responseCode)
|
|
467
476
|
map.putString("debugMessage", billingResult.debugMessage)
|
|
@@ -487,10 +496,7 @@ class RNIapModule(
|
|
|
487
496
|
billingClient.consumeAsync(
|
|
488
497
|
params
|
|
489
498
|
) { billingResult: BillingResult, purchaseToken: String? ->
|
|
490
|
-
if (billingResult
|
|
491
|
-
PlayUtils.instance
|
|
492
|
-
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
493
|
-
}
|
|
499
|
+
if (!isValidResult(billingResult, promise)) return@consumeAsync
|
|
494
500
|
|
|
495
501
|
val map = Arguments.createMap()
|
|
496
502
|
map.putInt("responseCode", billingResult.responseCode)
|
|
@@ -575,14 +581,10 @@ class RNIapModule(
|
|
|
575
581
|
for (type in types) {
|
|
576
582
|
billingClient.queryPurchasesAsync(
|
|
577
583
|
type
|
|
578
|
-
) { billingResult: BillingResult, list: List<Purchase
|
|
579
|
-
|
|
584
|
+
) { billingResult: BillingResult, list: List<Purchase> ->
|
|
585
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
580
586
|
|
|
581
|
-
|
|
582
|
-
if (!purchase.isAcknowledged) {
|
|
583
|
-
unacknowledgedPurchases.add(purchase)
|
|
584
|
-
}
|
|
585
|
-
}
|
|
587
|
+
val unacknowledgedPurchases = list.filter { !it.isAcknowledged }
|
|
586
588
|
onPurchasesUpdated(billingResult, unacknowledgedPurchases)
|
|
587
589
|
}
|
|
588
590
|
}
|
|
@@ -6,16 +6,25 @@ import com.android.billingclient.api.BillingResult
|
|
|
6
6
|
import com.android.billingclient.api.ConsumeResponseListener
|
|
7
7
|
import com.android.billingclient.api.Purchase
|
|
8
8
|
import com.android.billingclient.api.PurchasesResponseListener
|
|
9
|
+
import com.android.billingclient.api.SkuDetailsResponseListener
|
|
10
|
+
import com.facebook.react.bridge.Arguments
|
|
9
11
|
import com.facebook.react.bridge.Promise
|
|
10
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
|
|
11
16
|
import com.google.android.gms.common.ConnectionResult
|
|
12
17
|
import com.google.android.gms.common.GoogleApiAvailability
|
|
13
18
|
import io.mockk.MockKAnnotations
|
|
14
19
|
import io.mockk.every
|
|
15
20
|
import io.mockk.impl.annotations.MockK
|
|
21
|
+
import io.mockk.just
|
|
16
22
|
import io.mockk.mockk
|
|
23
|
+
import io.mockk.mockkStatic
|
|
24
|
+
import io.mockk.runs
|
|
17
25
|
import io.mockk.slot
|
|
18
26
|
import io.mockk.verify
|
|
27
|
+
import org.junit.Assert.assertEquals
|
|
19
28
|
import org.junit.Assert.assertTrue
|
|
20
29
|
import org.junit.Before
|
|
21
30
|
import org.junit.Test
|
|
@@ -24,10 +33,13 @@ class RNIapModuleTest {
|
|
|
24
33
|
|
|
25
34
|
@MockK
|
|
26
35
|
lateinit var context: ReactApplicationContext
|
|
36
|
+
|
|
27
37
|
@MockK
|
|
28
38
|
lateinit var builder: BillingClient.Builder
|
|
39
|
+
|
|
29
40
|
@MockK
|
|
30
41
|
lateinit var billingClient: BillingClient
|
|
42
|
+
|
|
31
43
|
@MockK
|
|
32
44
|
lateinit var availability: GoogleApiAvailability
|
|
33
45
|
|
|
@@ -67,7 +79,10 @@ class RNIapModuleTest {
|
|
|
67
79
|
every { billingClient.isReady } returns false
|
|
68
80
|
val listener = slot<BillingClientStateListener>()
|
|
69
81
|
every { billingClient.startConnection(capture(listener)) } answers {
|
|
70
|
-
listener.captured.onBillingSetupFinished(
|
|
82
|
+
listener.captured.onBillingSetupFinished(
|
|
83
|
+
BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK)
|
|
84
|
+
.build()
|
|
85
|
+
)
|
|
71
86
|
}
|
|
72
87
|
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
73
88
|
val promise = mockk<Promise>(relaxed = true)
|
|
@@ -82,7 +97,10 @@ class RNIapModuleTest {
|
|
|
82
97
|
every { billingClient.isReady } returns false
|
|
83
98
|
val listener = slot<BillingClientStateListener>()
|
|
84
99
|
every { billingClient.startConnection(capture(listener)) } answers {
|
|
85
|
-
listener.captured.onBillingSetupFinished(
|
|
100
|
+
listener.captured.onBillingSetupFinished(
|
|
101
|
+
BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ERROR)
|
|
102
|
+
.build()
|
|
103
|
+
)
|
|
86
104
|
}
|
|
87
105
|
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
88
106
|
val promise = mockk<Promise>(relaxed = true)
|
|
@@ -137,7 +155,11 @@ class RNIapModuleTest {
|
|
|
137
155
|
}
|
|
138
156
|
val consumeListener = slot<ConsumeResponseListener>()
|
|
139
157
|
every { billingClient.consumeAsync(any(), capture(consumeListener)) } answers {
|
|
140
|
-
consumeListener.captured.onConsumeResponse(
|
|
158
|
+
consumeListener.captured.onConsumeResponse(
|
|
159
|
+
BillingResult.newBuilder()
|
|
160
|
+
.setResponseCode(BillingClient.BillingResponseCode.ITEM_NOT_OWNED).build(),
|
|
161
|
+
""
|
|
162
|
+
)
|
|
141
163
|
}
|
|
142
164
|
|
|
143
165
|
module.flushFailedPurchasesCachedAsPending(promise)
|
|
@@ -164,6 +186,55 @@ class RNIapModuleTest {
|
|
|
164
186
|
|
|
165
187
|
@Test
|
|
166
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)
|
|
167
238
|
}
|
|
168
239
|
|
|
169
240
|
@Test
|