react-native-iap 8.2.0 → 8.3.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.
- package/.yarn/install-state.gz +0 -0
- package/RNIap.podspec +5 -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/PlayUtils.kt +1 -1
- package/android/src/play/java/com/dooboolab/RNIap/PromiseUtlis.kt +33 -0
- package/android/src/play/java/com/dooboolab/RNIap/RNIapModule.kt +182 -196
- package/android/src/testPlay/java/com/dooboolab/RNIap/RNIapModuleTest.kt +144 -5
- package/ios/RNIapQueue.swift +4 -3
- package/package.json +1 -1
- package/src/iap.js +13 -4
package/.yarn/install-state.gz
CHANGED
|
Binary file
|
package/RNIap.podspec
CHANGED
|
@@ -11,6 +11,11 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.platforms = { :ios => "9.0", :tvos => "9.0" }
|
|
12
12
|
s.source = { :git => "https://github.com/dooboolab/react-native-iap.git", :tag => "#{s.version}" }
|
|
13
13
|
s.source_files = "ios/*.{h,m,swift}"
|
|
14
|
+
s.script_phase = {
|
|
15
|
+
:name => 'Copy Swift Header',
|
|
16
|
+
:script => 'ditto "${DERIVED_SOURCES_DIR}/${PRODUCT_MODULE_NAME}-Swift.h" "${PODS_ROOT}/Headers/Public/${PRODUCT_MODULE_NAME}/${PRODUCT_MODULE_NAME}-Swift.h"',
|
|
17
|
+
:execution_position => :after_compile
|
|
18
|
+
}
|
|
14
19
|
s.swift_version = "4.2"
|
|
15
20
|
s.requires_arc = true
|
|
16
21
|
|
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
|
}
|
|
@@ -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.
|
|
10
|
+
promise.safeReject(errorData[0], errorData[1])
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
fun getBillingResponseData(responseCode: Int): Array<String?> {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.dooboolab.RNIap
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.bridge.ObjectAlreadyConsumedException
|
|
5
|
+
import com.facebook.react.bridge.Promise
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extension functions used to simplify promise handling since we don't
|
|
9
|
+
* want to crash in the case of it being resolved/rejected more than once
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
fun Promise.safeResolve(value: Any) {
|
|
13
|
+
try {
|
|
14
|
+
this.resolve(value)
|
|
15
|
+
} catch (oce: ObjectAlreadyConsumedException) {
|
|
16
|
+
Log.d(RNIapModule.TAG, "Already consumed ${oce.message}")
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fun Promise.safeReject(message: String) = this.safeReject(message, null, null)
|
|
21
|
+
|
|
22
|
+
fun Promise.safeReject(code: String?, message: String?) = this.safeReject(code, message, null)
|
|
23
|
+
|
|
24
|
+
fun Promise.safeReject(code: String?, throwable: Throwable?) =
|
|
25
|
+
this.safeReject(code, null, throwable)
|
|
26
|
+
|
|
27
|
+
fun Promise.safeReject(code: String?, message: String?, throwable: Throwable?) {
|
|
28
|
+
try {
|
|
29
|
+
this.reject(code, message, throwable)
|
|
30
|
+
} catch (oce: ObjectAlreadyConsumedException) {
|
|
31
|
+
Log.d(RNIapModule.TAG, "Already consumed ${oce.message}")
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -15,7 +15,6 @@ import com.android.billingclient.api.SkuDetails
|
|
|
15
15
|
import com.android.billingclient.api.SkuDetailsParams
|
|
16
16
|
import com.facebook.react.bridge.Arguments
|
|
17
17
|
import com.facebook.react.bridge.LifecycleEventListener
|
|
18
|
-
import com.facebook.react.bridge.ObjectAlreadyConsumedException
|
|
19
18
|
import com.facebook.react.bridge.Promise
|
|
20
19
|
import com.facebook.react.bridge.PromiseImpl
|
|
21
20
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
@@ -23,6 +22,7 @@ import com.facebook.react.bridge.ReactContext
|
|
|
23
22
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
24
23
|
import com.facebook.react.bridge.ReactMethod
|
|
25
24
|
import com.facebook.react.bridge.ReadableArray
|
|
25
|
+
import com.facebook.react.bridge.ReadableType
|
|
26
26
|
import com.facebook.react.bridge.WritableMap
|
|
27
27
|
import com.facebook.react.bridge.WritableNativeArray
|
|
28
28
|
import com.facebook.react.bridge.WritableNativeMap
|
|
@@ -40,28 +40,37 @@ class RNIapModule(
|
|
|
40
40
|
ReactContextBaseJavaModule(reactContext),
|
|
41
41
|
PurchasesUpdatedListener {
|
|
42
42
|
|
|
43
|
-
private var
|
|
43
|
+
private var billingClientCache: BillingClient? = null
|
|
44
44
|
private val skus: MutableMap<String, SkuDetails> = mutableMapOf()
|
|
45
45
|
override fun getName(): String {
|
|
46
|
-
return
|
|
46
|
+
return TAG
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
internal fun ensureConnection(
|
|
50
|
+
promise: Promise,
|
|
51
|
+
callback: (billingClient: BillingClient) -> Unit
|
|
52
|
+
) {
|
|
53
|
+
val billingClient = billingClientCache
|
|
54
|
+
if (billingClient?.isReady == true) {
|
|
55
|
+
callback(billingClient)
|
|
52
56
|
return
|
|
53
57
|
} else {
|
|
54
58
|
val nested = PromiseImpl(
|
|
55
59
|
{
|
|
56
60
|
if (it.isNotEmpty() && it[0] is Boolean && it[0] as Boolean) {
|
|
57
|
-
|
|
61
|
+
val connectedBillingClient = billingClientCache
|
|
62
|
+
if (connectedBillingClient?.isReady == true) {
|
|
63
|
+
callback(connectedBillingClient)
|
|
64
|
+
} else {
|
|
65
|
+
promise.safeReject(DoobooUtils.E_NOT_PREPARED, "Unable to auto-initialize connection")
|
|
66
|
+
}
|
|
58
67
|
} else {
|
|
59
68
|
Log.i(TAG, "Incorrect parameter in resolve")
|
|
60
69
|
}
|
|
61
70
|
},
|
|
62
71
|
{
|
|
63
72
|
if (it.size > 1 && it[0] is String && it[1] is String) {
|
|
64
|
-
promise.
|
|
73
|
+
promise.safeReject(
|
|
65
74
|
it[0] as String, it[1] as String
|
|
66
75
|
)
|
|
67
76
|
} else {
|
|
@@ -75,50 +84,44 @@ class RNIapModule(
|
|
|
75
84
|
|
|
76
85
|
@ReactMethod
|
|
77
86
|
fun initConnection(promise: Promise) {
|
|
78
|
-
if (billingClient.isReady) {
|
|
79
|
-
Log.i(
|
|
80
|
-
TAG,
|
|
81
|
-
"Already initialized, you should only call initConnection() once when your app starts"
|
|
82
|
-
)
|
|
83
|
-
promise.resolve(true)
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
87
|
if (googleApiAvailability.isGooglePlayServicesAvailable(reactContext)
|
|
87
88
|
!= ConnectionResult.SUCCESS
|
|
88
89
|
) {
|
|
89
90
|
Log.i(TAG, "Google Play Services are not available on this device")
|
|
90
|
-
promise.
|
|
91
|
+
promise.safeReject(DoobooUtils.E_NOT_PREPARED, "Google Play Services are not available on this device")
|
|
91
92
|
return
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
if (billingClientCache?.isReady == true) {
|
|
96
|
+
Log.i(
|
|
97
|
+
TAG,
|
|
98
|
+
"Already initialized, you should only call initConnection() once when your app starts"
|
|
99
|
+
)
|
|
100
|
+
promise.safeResolve(true)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
builder.setListener(this).build().also {
|
|
104
|
+
billingClientCache = it
|
|
105
|
+
it.startConnection(
|
|
106
|
+
object : BillingClientStateListener {
|
|
107
|
+
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
108
|
+
if (!isValidResult(billingResult, promise)) return
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
object : BillingClientStateListener {
|
|
98
|
-
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
99
|
-
val responseCode = billingResult.responseCode
|
|
100
|
-
try {
|
|
101
|
-
if (responseCode == BillingClient.BillingResponseCode.OK) {
|
|
102
|
-
promise.resolve(true)
|
|
103
|
-
} else {
|
|
104
|
-
PlayUtils.instance
|
|
105
|
-
.rejectPromiseWithBillingError(promise, responseCode)
|
|
106
|
-
}
|
|
107
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
108
|
-
Log.e(TAG, oce.message!!)
|
|
110
|
+
promise.safeResolve(true)
|
|
109
111
|
}
|
|
110
|
-
}
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
override fun onBillingServiceDisconnected() {
|
|
114
|
+
Log.i(TAG, "Billing service disconnected")
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
@ReactMethod
|
|
119
121
|
fun endConnection(promise: Promise) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
billingClientCache?.endConnection()
|
|
123
|
+
billingClientCache = null
|
|
124
|
+
promise.safeResolve(true)
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
private fun consumeItems(
|
|
@@ -129,7 +132,7 @@ class RNIapModule(
|
|
|
129
132
|
for (purchase in purchases) {
|
|
130
133
|
ensureConnection(
|
|
131
134
|
promise
|
|
132
|
-
) {
|
|
135
|
+
) { billingClient ->
|
|
133
136
|
val consumeParams =
|
|
134
137
|
ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
|
|
135
138
|
.build()
|
|
@@ -143,11 +146,8 @@ class RNIapModule(
|
|
|
143
146
|
)
|
|
144
147
|
return@ConsumeResponseListener
|
|
145
148
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
149
|
-
promise.reject(oce.message)
|
|
150
|
-
}
|
|
149
|
+
|
|
150
|
+
promise.safeResolve(true)
|
|
151
151
|
}
|
|
152
152
|
billingClient.consumeAsync(consumeParams, listener)
|
|
153
153
|
}
|
|
@@ -158,13 +158,14 @@ class RNIapModule(
|
|
|
158
158
|
fun flushFailedPurchasesCachedAsPending(promise: Promise) {
|
|
159
159
|
ensureConnection(
|
|
160
160
|
promise
|
|
161
|
-
) {
|
|
161
|
+
) { billingClient ->
|
|
162
162
|
billingClient.queryPurchasesAsync(
|
|
163
163
|
BillingClient.SkuType.INAPP
|
|
164
|
-
) {
|
|
164
|
+
) { billingResult: BillingResult, list: List<Purchase>? ->
|
|
165
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
165
166
|
if (list == null) {
|
|
166
167
|
// No purchases found
|
|
167
|
-
promise.
|
|
168
|
+
promise.safeResolve(false)
|
|
168
169
|
return@queryPurchasesAsync
|
|
169
170
|
}
|
|
170
171
|
// we only want to try to consume PENDING items, in order to force cache-refresh
|
|
@@ -172,7 +173,7 @@ class RNIapModule(
|
|
|
172
173
|
val pendingPurchases = list.filter { it.purchaseState == Purchase.PurchaseState.PENDING }
|
|
173
174
|
|
|
174
175
|
if (pendingPurchases.isEmpty()) {
|
|
175
|
-
promise.
|
|
176
|
+
promise.safeResolve(false)
|
|
176
177
|
return@queryPurchasesAsync
|
|
177
178
|
}
|
|
178
179
|
consumeItems(
|
|
@@ -185,101 +186,109 @@ class RNIapModule(
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
@ReactMethod
|
|
188
|
-
fun getItemsByType(type: String
|
|
189
|
+
fun getItemsByType(type: String, skuArr: ReadableArray, promise: Promise) {
|
|
189
190
|
ensureConnection(
|
|
190
191
|
promise
|
|
191
|
-
) {
|
|
192
|
+
) { billingClient ->
|
|
192
193
|
val skuList = ArrayList<String>()
|
|
193
194
|
for (i in 0 until skuArr.size()) {
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
if (skuArr.getType(i) == ReadableType.String) {
|
|
196
|
+
val sku = skuArr.getString(i)
|
|
196
197
|
skuList.add(sku)
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
val params = SkuDetailsParams.newBuilder()
|
|
200
|
-
params.setSkusList(skuList).setType(type
|
|
201
|
+
params.setSkusList(skuList).setType(type)
|
|
201
202
|
billingClient.querySkuDetailsAsync(
|
|
202
203
|
params.build()
|
|
203
204
|
) { billingResult: BillingResult, skuDetailsList: List<SkuDetails>? ->
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
208
|
-
return@querySkuDetailsAsync
|
|
209
|
-
}
|
|
205
|
+
if (!isValidResult(billingResult, promise)) return@querySkuDetailsAsync
|
|
206
|
+
|
|
207
|
+
val items = Arguments.createArray()
|
|
210
208
|
if (skuDetailsList != null) {
|
|
211
|
-
for (
|
|
212
|
-
skus[
|
|
209
|
+
for (skuDetails in skuDetailsList) {
|
|
210
|
+
skus[skuDetails.sku] = skuDetails
|
|
211
|
+
|
|
212
|
+
val item = Arguments.createMap()
|
|
213
|
+
item.putString("productId", skuDetails.sku)
|
|
214
|
+
val introductoryPriceMicros = skuDetails.introductoryPriceAmountMicros
|
|
215
|
+
val priceAmountMicros = skuDetails.priceAmountMicros
|
|
216
|
+
// Use valueOf instead of constructors.
|
|
217
|
+
// See:
|
|
218
|
+
// https://www.javaworld.com/article/2073176/caution--double-to-bigdecimal-in-java.html
|
|
219
|
+
val priceAmount = BigDecimal.valueOf(priceAmountMicros)
|
|
220
|
+
val introductoryPriceAmount =
|
|
221
|
+
BigDecimal.valueOf(introductoryPriceMicros)
|
|
222
|
+
val microUnitsDivisor = BigDecimal.valueOf(1000000)
|
|
223
|
+
val price = priceAmount.divide(microUnitsDivisor).toString()
|
|
224
|
+
val introductoryPriceAsAmountAndroid =
|
|
225
|
+
introductoryPriceAmount.divide(microUnitsDivisor).toString()
|
|
226
|
+
item.putString("price", price)
|
|
227
|
+
item.putString("currency", skuDetails.priceCurrencyCode)
|
|
228
|
+
item.putString("type", skuDetails.type)
|
|
229
|
+
item.putString("localizedPrice", skuDetails.price)
|
|
230
|
+
item.putString("title", skuDetails.title)
|
|
231
|
+
item.putString("description", skuDetails.description)
|
|
232
|
+
item.putString("introductoryPrice", skuDetails.introductoryPrice)
|
|
233
|
+
item.putString("typeAndroid", skuDetails.type)
|
|
234
|
+
item.putString("packageNameAndroid", skuDetails.zzc())
|
|
235
|
+
item.putString("originalPriceAndroid", skuDetails.originalPrice)
|
|
236
|
+
item.putString(
|
|
237
|
+
"subscriptionPeriodAndroid",
|
|
238
|
+
skuDetails.subscriptionPeriod
|
|
239
|
+
)
|
|
240
|
+
item.putString("freeTrialPeriodAndroid", skuDetails.freeTrialPeriod)
|
|
241
|
+
item.putString(
|
|
242
|
+
"introductoryPriceCyclesAndroid",
|
|
243
|
+
skuDetails.introductoryPriceCycles.toString()
|
|
244
|
+
)
|
|
245
|
+
item.putString(
|
|
246
|
+
"introductoryPricePeriodAndroid", skuDetails.introductoryPricePeriod
|
|
247
|
+
)
|
|
248
|
+
item.putString(
|
|
249
|
+
"introductoryPriceAsAmountAndroid", introductoryPriceAsAmountAndroid
|
|
250
|
+
)
|
|
251
|
+
item.putString("iconUrl", skuDetails.iconUrl)
|
|
252
|
+
item.putString("originalJson", skuDetails.originalJson)
|
|
253
|
+
val originalPriceAmountMicros =
|
|
254
|
+
BigDecimal.valueOf(skuDetails.originalPriceAmountMicros)
|
|
255
|
+
val originalPrice =
|
|
256
|
+
originalPriceAmountMicros.divide(microUnitsDivisor).toString()
|
|
257
|
+
item.putString("originalPrice", originalPrice)
|
|
258
|
+
items.pushMap(item)
|
|
213
259
|
}
|
|
214
260
|
}
|
|
215
|
-
|
|
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
|
-
}
|
|
261
|
+
promise.safeResolve(items)
|
|
270
262
|
}
|
|
271
263
|
}
|
|
272
264
|
}
|
|
273
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Rejects promise with billing code if BillingResult is not OK
|
|
268
|
+
*/
|
|
269
|
+
private fun isValidResult(
|
|
270
|
+
billingResult: BillingResult,
|
|
271
|
+
promise: Promise
|
|
272
|
+
): Boolean {
|
|
273
|
+
Log.d(TAG, "responseCode: " + billingResult.responseCode)
|
|
274
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
275
|
+
PlayUtils.instance
|
|
276
|
+
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
return true
|
|
280
|
+
}
|
|
281
|
+
|
|
274
282
|
@ReactMethod
|
|
275
283
|
fun getAvailableItemsByType(type: String, promise: Promise) {
|
|
276
284
|
ensureConnection(
|
|
277
285
|
promise
|
|
278
|
-
) {
|
|
286
|
+
) { billingClient ->
|
|
279
287
|
val items = WritableNativeArray()
|
|
280
288
|
billingClient.queryPurchasesAsync(
|
|
281
289
|
if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
|
|
282
|
-
) { billingResult: BillingResult
|
|
290
|
+
) { billingResult: BillingResult, purchases: List<Purchase>? ->
|
|
291
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
283
292
|
if (purchases != null) {
|
|
284
293
|
for (i in purchases.indices) {
|
|
285
294
|
val purchase = purchases[i]
|
|
@@ -297,11 +306,11 @@ class RNIapModule(
|
|
|
297
306
|
item.putString("packageNameAndroid", purchase.packageName)
|
|
298
307
|
item.putString(
|
|
299
308
|
"obfuscatedAccountIdAndroid",
|
|
300
|
-
purchase.accountIdentifiers
|
|
309
|
+
purchase.accountIdentifiers?.obfuscatedAccountId
|
|
301
310
|
)
|
|
302
311
|
item.putString(
|
|
303
312
|
"obfuscatedProfileIdAndroid",
|
|
304
|
-
purchase.accountIdentifiers
|
|
313
|
+
purchase.accountIdentifiers?.obfuscatedProfileId
|
|
305
314
|
)
|
|
306
315
|
if (type == BillingClient.SkuType.SUBS) {
|
|
307
316
|
item.putBoolean("autoRenewingAndroid", purchase.isAutoRenewing)
|
|
@@ -309,11 +318,7 @@ class RNIapModule(
|
|
|
309
318
|
items.pushMap(item)
|
|
310
319
|
}
|
|
311
320
|
}
|
|
312
|
-
|
|
313
|
-
promise.resolve(items)
|
|
314
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
315
|
-
Log.e(TAG, oce.message!!)
|
|
316
|
-
}
|
|
321
|
+
promise.safeResolve(items)
|
|
317
322
|
}
|
|
318
323
|
}
|
|
319
324
|
}
|
|
@@ -322,20 +327,16 @@ class RNIapModule(
|
|
|
322
327
|
fun getPurchaseHistoryByType(type: String, promise: Promise) {
|
|
323
328
|
ensureConnection(
|
|
324
329
|
promise
|
|
325
|
-
) {
|
|
330
|
+
) { billingClient ->
|
|
326
331
|
billingClient.queryPurchaseHistoryAsync(
|
|
327
332
|
if (type == "subs") BillingClient.SkuType.SUBS else BillingClient.SkuType.INAPP
|
|
328
333
|
) { billingResult, purchaseHistoryRecordList ->
|
|
329
|
-
if (billingResult
|
|
330
|
-
|
|
331
|
-
.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
332
|
-
return@queryPurchaseHistoryAsync
|
|
333
|
-
}
|
|
334
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchaseHistoryAsync
|
|
335
|
+
|
|
334
336
|
Log.d(TAG, purchaseHistoryRecordList.toString())
|
|
335
337
|
val items = Arguments.createArray()
|
|
336
|
-
|
|
338
|
+
purchaseHistoryRecordList?.forEach { purchase ->
|
|
337
339
|
val item = Arguments.createMap()
|
|
338
|
-
val purchase = purchaseHistoryRecordList[i]
|
|
339
340
|
item.putString("productId", purchase.skus[0])
|
|
340
341
|
item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
|
|
341
342
|
item.putString("transactionReceipt", purchase.originalJson)
|
|
@@ -345,11 +346,7 @@ class RNIapModule(
|
|
|
345
346
|
item.putString("developerPayload", purchase.developerPayload)
|
|
346
347
|
items.pushMap(item)
|
|
347
348
|
}
|
|
348
|
-
|
|
349
|
-
promise.resolve(items)
|
|
350
|
-
} catch (oce: ObjectAlreadyConsumedException) {
|
|
351
|
-
Log.e(TAG, oce.message!!)
|
|
352
|
-
}
|
|
349
|
+
promise.safeResolve(items)
|
|
353
350
|
}
|
|
354
351
|
}
|
|
355
352
|
}
|
|
@@ -366,12 +363,12 @@ class RNIapModule(
|
|
|
366
363
|
) {
|
|
367
364
|
val activity = currentActivity
|
|
368
365
|
if (activity == null) {
|
|
369
|
-
promise.
|
|
366
|
+
promise.safeReject(DoobooUtils.E_UNKNOWN, "getCurrentActivity returned null")
|
|
370
367
|
return
|
|
371
368
|
}
|
|
372
369
|
ensureConnection(
|
|
373
370
|
promise
|
|
374
|
-
) {
|
|
371
|
+
) { billingClient ->
|
|
375
372
|
DoobooUtils.instance.addPromiseForKey(
|
|
376
373
|
PROMISE_BUY_ITEM, promise
|
|
377
374
|
)
|
|
@@ -386,7 +383,7 @@ class RNIapModule(
|
|
|
386
383
|
error.putString("message", debugMessage)
|
|
387
384
|
error.putString("productId", sku)
|
|
388
385
|
sendEvent(reactContext, "purchase-error", error)
|
|
389
|
-
promise.
|
|
386
|
+
promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
|
|
390
387
|
return@ensureConnection
|
|
391
388
|
}
|
|
392
389
|
builder.setSkuDetails(selectedSku)
|
|
@@ -419,7 +416,7 @@ class RNIapModule(
|
|
|
419
416
|
error.putString("message", debugMessage)
|
|
420
417
|
error.putString("productId", sku)
|
|
421
418
|
sendEvent(reactContext, "purchase-error", error)
|
|
422
|
-
promise.
|
|
419
|
+
promise.safeReject(PROMISE_BUY_ITEM, debugMessage)
|
|
423
420
|
return@ensureConnection
|
|
424
421
|
}
|
|
425
422
|
} else if (prorationMode
|
|
@@ -455,77 +452,71 @@ class RNIapModule(
|
|
|
455
452
|
builder.setSubscriptionUpdateParams(subscriptionUpdateParams)
|
|
456
453
|
}
|
|
457
454
|
val flowParams = builder.build()
|
|
458
|
-
val
|
|
459
|
-
|
|
460
|
-
|
|
455
|
+
val billingResultCode = billingClient.launchBillingFlow(activity, flowParams)?.responseCode ?: BillingClient.BillingResponseCode.ERROR
|
|
456
|
+
if (billingResultCode == BillingClient.BillingResponseCode.OK) {
|
|
457
|
+
promise.safeResolve(true)
|
|
458
|
+
return@ensureConnection
|
|
459
|
+
} else {
|
|
460
|
+
val errorData: Array<String?> =
|
|
461
|
+
PlayUtils.instance.getBillingResponseData(billingResultCode)
|
|
462
|
+
promise.safeReject(errorData[0], errorData[1])
|
|
463
|
+
}
|
|
461
464
|
}
|
|
462
465
|
}
|
|
463
466
|
|
|
464
467
|
@ReactMethod
|
|
465
468
|
fun acknowledgePurchase(
|
|
466
|
-
token: String
|
|
469
|
+
token: String,
|
|
467
470
|
developerPayLoad: String?,
|
|
468
471
|
promise: Promise
|
|
469
472
|
) {
|
|
470
473
|
ensureConnection(
|
|
471
474
|
promise
|
|
472
|
-
) {
|
|
475
|
+
) { billingClient ->
|
|
473
476
|
val acknowledgePurchaseParams =
|
|
474
477
|
AcknowledgePurchaseParams.newBuilder().setPurchaseToken(
|
|
475
|
-
token
|
|
478
|
+
token
|
|
476
479
|
).build()
|
|
477
480
|
billingClient.acknowledgePurchase(
|
|
478
481
|
acknowledgePurchaseParams
|
|
479
482
|
) { billingResult: BillingResult ->
|
|
480
|
-
if (billingResult
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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!!)
|
|
495
|
-
}
|
|
483
|
+
if (!isValidResult(billingResult, promise)) return@acknowledgePurchase
|
|
484
|
+
|
|
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.safeResolve(map)
|
|
496
493
|
}
|
|
497
494
|
}
|
|
498
495
|
}
|
|
499
496
|
|
|
500
497
|
@ReactMethod
|
|
501
498
|
fun consumeProduct(
|
|
502
|
-
token: String
|
|
499
|
+
token: String,
|
|
503
500
|
developerPayLoad: String?,
|
|
504
501
|
promise: Promise
|
|
505
502
|
) {
|
|
506
|
-
val params = ConsumeParams.newBuilder().setPurchaseToken(token
|
|
503
|
+
val params = ConsumeParams.newBuilder().setPurchaseToken(token).build()
|
|
507
504
|
ensureConnection(
|
|
508
505
|
promise
|
|
509
|
-
) {
|
|
506
|
+
) { billingClient ->
|
|
510
507
|
billingClient.consumeAsync(
|
|
511
508
|
params
|
|
512
509
|
) { billingResult: BillingResult, purchaseToken: String? ->
|
|
513
|
-
if (billingResult
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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)
|
|
528
|
-
}
|
|
510
|
+
if (!isValidResult(billingResult, promise)) return@consumeAsync
|
|
511
|
+
|
|
512
|
+
val map = Arguments.createMap()
|
|
513
|
+
map.putInt("responseCode", billingResult.responseCode)
|
|
514
|
+
map.putString("debugMessage", billingResult.debugMessage)
|
|
515
|
+
val errorData: Array<String?> = PlayUtils.instance
|
|
516
|
+
.getBillingResponseData(billingResult.responseCode)
|
|
517
|
+
map.putString("code", errorData[0])
|
|
518
|
+
map.putString("message", errorData[1])
|
|
519
|
+
promise.safeResolve(map)
|
|
529
520
|
}
|
|
530
521
|
}
|
|
531
522
|
}
|
|
@@ -596,23 +587,19 @@ class RNIapModule(
|
|
|
596
587
|
private fun sendUnconsumedPurchases(promise: Promise) {
|
|
597
588
|
ensureConnection(
|
|
598
589
|
promise
|
|
599
|
-
) {
|
|
590
|
+
) { billingClient ->
|
|
600
591
|
val types = arrayOf(BillingClient.SkuType.INAPP, BillingClient.SkuType.SUBS)
|
|
601
592
|
for (type in types) {
|
|
602
593
|
billingClient.queryPurchasesAsync(
|
|
603
594
|
type
|
|
604
|
-
) { billingResult: BillingResult, list: List<Purchase
|
|
605
|
-
|
|
595
|
+
) { billingResult: BillingResult, list: List<Purchase> ->
|
|
596
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
606
597
|
|
|
607
|
-
|
|
608
|
-
if (!purchase.isAcknowledged) {
|
|
609
|
-
unacknowledgedPurchases.add(purchase)
|
|
610
|
-
}
|
|
611
|
-
}
|
|
598
|
+
val unacknowledgedPurchases = list.filter { !it.isAcknowledged }
|
|
612
599
|
onPurchasesUpdated(billingResult, unacknowledgedPurchases)
|
|
613
600
|
}
|
|
614
601
|
}
|
|
615
|
-
promise.
|
|
602
|
+
promise.safeResolve(true)
|
|
616
603
|
}
|
|
617
604
|
}
|
|
618
605
|
|
|
@@ -631,9 +618,8 @@ class RNIapModule(
|
|
|
631
618
|
// Keep: Required for RN built-in Event Emitter Calls.
|
|
632
619
|
}
|
|
633
620
|
|
|
634
|
-
@
|
|
635
|
-
|
|
636
|
-
get() = reactApplicationContext.packageName
|
|
621
|
+
@ReactMethod
|
|
622
|
+
fun getPackageName(promise: Promise) = promise.resolve(reactApplicationContext.packageName)
|
|
637
623
|
|
|
638
624
|
private fun sendEvent(
|
|
639
625
|
reactContext: ReactContext,
|
|
@@ -655,7 +641,7 @@ class RNIapModule(
|
|
|
655
641
|
override fun onHostResume() {}
|
|
656
642
|
override fun onHostPause() {}
|
|
657
643
|
override fun onHostDestroy() {
|
|
658
|
-
|
|
644
|
+
billingClientCache?.endConnection()
|
|
659
645
|
}
|
|
660
646
|
}
|
|
661
647
|
reactContext.addLifecycleEventListener(lifecycleEventListener)
|
|
@@ -3,17 +3,30 @@ package com.dooboolab.RNIap
|
|
|
3
3
|
import com.android.billingclient.api.BillingClient
|
|
4
4
|
import com.android.billingclient.api.BillingClientStateListener
|
|
5
5
|
import com.android.billingclient.api.BillingResult
|
|
6
|
+
import com.android.billingclient.api.ConsumeResponseListener
|
|
7
|
+
import com.android.billingclient.api.Purchase
|
|
6
8
|
import com.android.billingclient.api.PurchasesResponseListener
|
|
9
|
+
import com.android.billingclient.api.SkuDetailsResponseListener
|
|
10
|
+
import com.facebook.react.bridge.Arguments
|
|
7
11
|
import com.facebook.react.bridge.Promise
|
|
8
12
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
13
|
+
import com.facebook.react.bridge.ReadableArray
|
|
14
|
+
import com.facebook.react.bridge.ReadableType
|
|
15
|
+
import com.facebook.react.bridge.WritableArray
|
|
16
|
+
import com.facebook.react.bridge.WritableMap
|
|
9
17
|
import com.google.android.gms.common.ConnectionResult
|
|
10
18
|
import com.google.android.gms.common.GoogleApiAvailability
|
|
11
19
|
import io.mockk.MockKAnnotations
|
|
12
20
|
import io.mockk.every
|
|
13
21
|
import io.mockk.impl.annotations.MockK
|
|
22
|
+
import io.mockk.just
|
|
14
23
|
import io.mockk.mockk
|
|
24
|
+
import io.mockk.mockkStatic
|
|
25
|
+
import io.mockk.runs
|
|
15
26
|
import io.mockk.slot
|
|
16
27
|
import io.mockk.verify
|
|
28
|
+
import org.junit.Assert.assertEquals
|
|
29
|
+
import org.junit.Assert.assertTrue
|
|
17
30
|
import org.junit.Before
|
|
18
31
|
import org.junit.Test
|
|
19
32
|
|
|
@@ -21,10 +34,13 @@ class RNIapModuleTest {
|
|
|
21
34
|
|
|
22
35
|
@MockK
|
|
23
36
|
lateinit var context: ReactApplicationContext
|
|
37
|
+
|
|
24
38
|
@MockK
|
|
25
39
|
lateinit var builder: BillingClient.Builder
|
|
40
|
+
|
|
26
41
|
@MockK
|
|
27
42
|
lateinit var billingClient: BillingClient
|
|
43
|
+
|
|
28
44
|
@MockK
|
|
29
45
|
lateinit var availability: GoogleApiAvailability
|
|
30
46
|
|
|
@@ -40,9 +56,13 @@ class RNIapModuleTest {
|
|
|
40
56
|
|
|
41
57
|
@Test
|
|
42
58
|
fun `initConnection Already connected should resolve to true`() {
|
|
59
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
60
|
+
module.initConnection(mockk())
|
|
61
|
+
|
|
43
62
|
every { billingClient.isReady } returns true
|
|
44
|
-
val promise = mockk<Promise>(relaxed = true)
|
|
45
63
|
|
|
64
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
65
|
+
// Already connected
|
|
46
66
|
module.initConnection(promise)
|
|
47
67
|
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
48
68
|
verify { promise.resolve(true) }
|
|
@@ -55,7 +75,7 @@ class RNIapModuleTest {
|
|
|
55
75
|
val promise = mockk<Promise>(relaxed = true)
|
|
56
76
|
|
|
57
77
|
module.initConnection(promise)
|
|
58
|
-
verify { promise.
|
|
78
|
+
verify { promise.safeReject(any(), any<String>()) }
|
|
59
79
|
verify(exactly = 0) { promise.resolve(any()) }
|
|
60
80
|
}
|
|
61
81
|
|
|
@@ -64,7 +84,10 @@ class RNIapModuleTest {
|
|
|
64
84
|
every { billingClient.isReady } returns false
|
|
65
85
|
val listener = slot<BillingClientStateListener>()
|
|
66
86
|
every { billingClient.startConnection(capture(listener)) } answers {
|
|
67
|
-
listener.captured.onBillingSetupFinished(
|
|
87
|
+
listener.captured.onBillingSetupFinished(
|
|
88
|
+
BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK)
|
|
89
|
+
.build()
|
|
90
|
+
)
|
|
68
91
|
}
|
|
69
92
|
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
70
93
|
val promise = mockk<Promise>(relaxed = true)
|
|
@@ -79,20 +102,25 @@ class RNIapModuleTest {
|
|
|
79
102
|
every { billingClient.isReady } returns false
|
|
80
103
|
val listener = slot<BillingClientStateListener>()
|
|
81
104
|
every { billingClient.startConnection(capture(listener)) } answers {
|
|
82
|
-
listener.captured.onBillingSetupFinished(
|
|
105
|
+
listener.captured.onBillingSetupFinished(
|
|
106
|
+
BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ERROR)
|
|
107
|
+
.build()
|
|
108
|
+
)
|
|
83
109
|
}
|
|
84
110
|
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
85
111
|
val promise = mockk<Promise>(relaxed = true)
|
|
86
112
|
|
|
87
113
|
module.initConnection(promise)
|
|
88
|
-
verify { promise.
|
|
114
|
+
verify { promise.safeReject(any(), any<String>()) }
|
|
89
115
|
verify(exactly = 0) { promise.resolve(any()) }
|
|
90
116
|
}
|
|
91
117
|
|
|
92
118
|
@Test
|
|
93
119
|
fun `endConnection resolves`() {
|
|
120
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
94
121
|
val promise = mockk<Promise>(relaxed = true)
|
|
95
122
|
|
|
123
|
+
module.initConnection(mockk())
|
|
96
124
|
module.endConnection(promise)
|
|
97
125
|
|
|
98
126
|
verify { billingClient.endConnection() }
|
|
@@ -102,20 +130,131 @@ class RNIapModuleTest {
|
|
|
102
130
|
|
|
103
131
|
@Test
|
|
104
132
|
fun `flushFailedPurchasesCachedAsPending resolves to false if no pending purchases`() {
|
|
133
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
105
134
|
every { billingClient.isReady } returns true
|
|
106
135
|
val promise = mockk<Promise>(relaxed = true)
|
|
107
136
|
val listener = slot<PurchasesResponseListener>()
|
|
108
137
|
every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers {
|
|
109
138
|
listener.captured.onQueryPurchasesResponse(BillingResult.newBuilder().build(), listOf())
|
|
110
139
|
}
|
|
140
|
+
module.initConnection(mockk())
|
|
111
141
|
module.flushFailedPurchasesCachedAsPending(promise)
|
|
112
142
|
|
|
113
143
|
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
114
144
|
verify { promise.resolve(false) } // empty list
|
|
115
145
|
}
|
|
116
146
|
|
|
147
|
+
@Test
|
|
148
|
+
fun `flushFailedPurchasesCachedAsPending resolves to true if pending purchases`() {
|
|
149
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
150
|
+
every { billingClient.isReady } returns true
|
|
151
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
152
|
+
val listener = slot<PurchasesResponseListener>()
|
|
153
|
+
every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers {
|
|
154
|
+
listener.captured.onQueryPurchasesResponse(
|
|
155
|
+
BillingResult.newBuilder().build(),
|
|
156
|
+
listOf(
|
|
157
|
+
// 4 = Pending
|
|
158
|
+
mockk<Purchase> {
|
|
159
|
+
every { purchaseState } returns 2
|
|
160
|
+
every { purchaseToken } returns "token"
|
|
161
|
+
},
|
|
162
|
+
Purchase("", "1")
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
val consumeListener = slot<ConsumeResponseListener>()
|
|
167
|
+
every { billingClient.consumeAsync(any(), capture(consumeListener)) } answers {
|
|
168
|
+
consumeListener.captured.onConsumeResponse(
|
|
169
|
+
BillingResult.newBuilder()
|
|
170
|
+
.setResponseCode(BillingClient.BillingResponseCode.ITEM_NOT_OWNED).build(),
|
|
171
|
+
""
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
module.initConnection(mockk())
|
|
175
|
+
module.flushFailedPurchasesCachedAsPending(promise)
|
|
176
|
+
|
|
177
|
+
verify(exactly = 0) { promise.reject(any(), any<String>()) }
|
|
178
|
+
verify { promise.resolve(true) } // at least one pending transactions
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@Test
|
|
182
|
+
fun `ensureConnection should attempt to reconnect, if not in ready state`() {
|
|
183
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
184
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
185
|
+
var isCallbackCalled = false
|
|
186
|
+
val callback = { _: BillingClient ->
|
|
187
|
+
isCallbackCalled = true
|
|
188
|
+
promise.resolve(true)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
every { billingClient.isReady } returns true
|
|
192
|
+
val listener = slot<BillingClientStateListener>()
|
|
193
|
+
every { billingClient.startConnection(capture(listener)) } answers {
|
|
194
|
+
listener.captured.onBillingSetupFinished(
|
|
195
|
+
BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK)
|
|
196
|
+
.build()
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.ensureConnection(promise, callback)
|
|
201
|
+
verify { promise.resolve(true) } // at least one pending transactions
|
|
202
|
+
assertTrue("Should call callback", isCallbackCalled)
|
|
203
|
+
}
|
|
204
|
+
|
|
117
205
|
@Test
|
|
118
206
|
fun getItemsByType() {
|
|
207
|
+
every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS
|
|
208
|
+
every { billingClient.isReady } returns true
|
|
209
|
+
val promise = mockk<Promise>(relaxed = true)
|
|
210
|
+
val listener = slot<SkuDetailsResponseListener>()
|
|
211
|
+
every { billingClient.querySkuDetailsAsync(any(), capture(listener)) } answers {
|
|
212
|
+
listener.captured.onSkuDetailsResponse(
|
|
213
|
+
BillingResult.newBuilder().build(),
|
|
214
|
+
listOf(
|
|
215
|
+
mockk {
|
|
216
|
+
every { sku } returns "sku1"
|
|
217
|
+
every { introductoryPriceAmountMicros } returns 0
|
|
218
|
+
every { priceAmountMicros } returns 1
|
|
219
|
+
every { priceCurrencyCode } returns "USD"
|
|
220
|
+
every { type } returns "sub"
|
|
221
|
+
every { price } returns "$10.0"
|
|
222
|
+
every { title } returns "My product"
|
|
223
|
+
every { description } returns "My desc"
|
|
224
|
+
every { introductoryPrice } returns "$5.0"
|
|
225
|
+
every { zzc() } returns "com.mypackage"
|
|
226
|
+
every { originalPrice } returns "$13.0"
|
|
227
|
+
every { subscriptionPeriod } returns "3 months"
|
|
228
|
+
every { freeTrialPeriod } returns "1 week"
|
|
229
|
+
every { introductoryPriceCycles } returns 1
|
|
230
|
+
every { introductoryPricePeriod } returns "1"
|
|
231
|
+
every { iconUrl } returns "http://myicon.com/icon"
|
|
232
|
+
every { originalJson } returns "{}"
|
|
233
|
+
every { originalPriceAmountMicros } returns 2
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
val skus = mockk<ReadableArray>() {
|
|
239
|
+
every { size() } returns 1
|
|
240
|
+
every { getString(0) } returns "sku0"
|
|
241
|
+
every { getType(0) } returns ReadableType.String
|
|
242
|
+
}
|
|
243
|
+
mockkStatic(Arguments::class)
|
|
244
|
+
|
|
245
|
+
val itemsMap = mockk<WritableMap>()
|
|
246
|
+
val itemsArr = mockk<WritableArray>()
|
|
247
|
+
every { Arguments.createMap() } returns itemsMap
|
|
248
|
+
every { Arguments.createArray() } returns itemsArr
|
|
249
|
+
every { itemsMap.putString(any(), any()) } just runs
|
|
250
|
+
var itemsSize = 0
|
|
251
|
+
every { itemsArr.pushMap(any()) } answers {
|
|
252
|
+
itemsSize++
|
|
253
|
+
}
|
|
254
|
+
module.initConnection(mockk())
|
|
255
|
+
module.getItemsByType("subs", skus, promise)
|
|
256
|
+
verify { promise.resolve(any()) }
|
|
257
|
+
assertEquals(itemsSize, 1)
|
|
119
258
|
}
|
|
120
259
|
|
|
121
260
|
@Test
|
package/ios/RNIapQueue.swift
CHANGED
|
@@ -10,12 +10,13 @@ import StoreKit
|
|
|
10
10
|
|
|
11
11
|
// Temporarily stores payment information since it is sent by the OS before RN instantiates the RNModule
|
|
12
12
|
@objc(RNIapQueue)
|
|
13
|
-
class RNIapQueue: NSObject, SKPaymentTransactionObserver {
|
|
14
|
-
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
|
|
13
|
+
public class RNIapQueue: NSObject, SKPaymentTransactionObserver {
|
|
14
|
+
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
|
|
15
15
|
//No-op
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
@objc
|
|
19
|
+
public static let shared = RNIapQueue()
|
|
19
20
|
|
|
20
21
|
var queue: SKPaymentQueue? = nil;
|
|
21
22
|
var payment: SKPayment? = nil;
|
package/package.json
CHANGED
package/src/iap.js
CHANGED
|
@@ -400,10 +400,19 @@ export var acknowledgePurchaseAndroid = function (token, developerPayload) {
|
|
|
400
400
|
* @param {string} sku The product's SKU (on Android)
|
|
401
401
|
* @returns {Promise<void>}
|
|
402
402
|
*/
|
|
403
|
-
export var deepLinkToSubscriptionsAndroid = function (sku) {
|
|
404
|
-
|
|
405
|
-
return
|
|
406
|
-
|
|
403
|
+
export var deepLinkToSubscriptionsAndroid = function (sku) { return __awaiter(void 0, void 0, void 0, function () {
|
|
404
|
+
var _a, _b, _c;
|
|
405
|
+
return __generator(this, function (_d) {
|
|
406
|
+
switch (_d.label) {
|
|
407
|
+
case 0:
|
|
408
|
+
checkNativeAndroidAvailable();
|
|
409
|
+
_b = (_a = Linking).openURL;
|
|
410
|
+
_c = "https://play.google.com/store/account/subscriptions?package=".concat;
|
|
411
|
+
return [4 /*yield*/, RNIapModule.getPackageName()];
|
|
412
|
+
case 1: return [2 /*return*/, _b.apply(_a, [_c.apply("https://play.google.com/store/account/subscriptions?package=", [_d.sent(), "&sku="]).concat(sku)])];
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}); };
|
|
407
416
|
/**
|
|
408
417
|
* Should Add Store Payment (iOS only)
|
|
409
418
|
* Indicates the the App Store purchase should continue from the app instead of the App Store.
|