expo-iap 1.0.3 → 2.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +9 -0
- package/.prettierrc.js +9 -0
- package/.swiftlint.yml +10 -0
- package/README.md +28 -21
- package/android/build.gradle +34 -72
- package/android/src/main/AndroidManifest.xml +1 -4
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +535 -0
- package/android/src/main/java/expo/modules/iap/MissingCurrentActivityException.kt +6 -0
- package/android/src/main/java/expo/modules/iap/PlayUtils.kt +124 -0
- package/build/ExpoIap.types.d.ts +89 -0
- package/build/ExpoIap.types.d.ts.map +1 -0
- package/build/ExpoIap.types.js +59 -0
- package/build/ExpoIap.types.js.map +1 -0
- package/build/ExpoIapModule.d.ts +3 -0
- package/build/ExpoIapModule.d.ts.map +1 -0
- package/build/ExpoIapModule.js +5 -0
- package/build/ExpoIapModule.js.map +1 -0
- package/build/index.d.ts +38 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +202 -0
- package/build/index.js.map +1 -0
- package/build/modules/android.d.ts +40 -0
- package/build/modules/android.d.ts.map +1 -0
- package/build/modules/android.js +54 -0
- package/build/modules/android.js.map +1 -0
- package/build/modules/ios.d.ts +41 -0
- package/build/modules/ios.d.ts.map +1 -0
- package/build/modules/ios.js +44 -0
- package/build/modules/ios.js.map +1 -0
- package/build/types/ExpoIapAndroid.types.d.ts +113 -0
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -0
- package/build/types/ExpoIapAndroid.types.js +23 -0
- package/build/types/ExpoIapAndroid.types.js.map +1 -0
- package/build/types/ExpoIapIos.types.d.ts +122 -0
- package/build/types/ExpoIapIos.types.d.ts.map +1 -0
- package/build/types/ExpoIapIos.types.js +2 -0
- package/build/types/ExpoIapIos.types.js.map +1 -0
- package/bun.lockb +0 -0
- package/expo-module.config.json +4 -8
- package/ios/ExpoIap.podspec +27 -0
- package/ios/ExpoIapModule.swift +498 -0
- package/ios/ProductStore.swift +27 -0
- package/ios/Types.swift +54 -0
- package/package.json +33 -62
- package/src/ExpoIap.types.ts +125 -0
- package/src/ExpoIapModule.ts +5 -0
- package/src/index.ts +397 -0
- package/src/modules/android.ts +89 -0
- package/src/modules/ios.ts +81 -0
- package/src/types/ExpoIapAndroid.types.ts +123 -0
- package/src/types/ExpoIapIos.types.ts +141 -0
- package/tsconfig.json +9 -0
- package/.editorconfig +0 -10
- package/.flowconfig +0 -11
- package/.monolinterrc +0 -3
- package/.yarn/install-state.gz +0 -0
- package/.yarn/releases/yarn-3.1.1.cjs +0 -768
- package/.yarnrc.yml +0 -3
- package/LICENSE +0 -21
- package/RNIap.podspec +0 -18
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +0 -6
- package/android/gradle.properties +0 -2
- package/android/gradlew +0 -160
- package/android/gradlew.bat +0 -90
- package/android/libs/in-app-purchasing-2.0.76.jar +0 -0
- package/android/src/amazon/AndroidManifest.xml +0 -12
- package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonListener.kt +0 -356
- package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonModule.kt +0 -128
- package/android/src/amazon/java/com/dooboolab/RNIap/RNIapPackage.kt +0 -20
- package/android/src/main/java/com/dooboolab/RNIap/DoobooUtils.kt +0 -180
- package/android/src/play/java/com/dooboolab/RNIap/PlayUtils.kt +0 -77
- package/android/src/play/java/com/dooboolab/RNIap/RNIapModule.kt +0 -698
- package/android/src/play/java/com/dooboolab/RNIap/RNIapPackage.kt +0 -20
- package/babel.config.js +0 -10
- package/index.d.ts +0 -3
- package/index.js +0 -3
- package/index.js.flow +0 -9
- package/ios/RNIap.xcodeproj/project.pbxproj +0 -370
- package/ios/RNIap.xcodeproj/xcshareddata/xcschemes/RNIap.xcscheme +0 -80
- package/ios/RNIapIos.m +0 -60
- package/ios/RNIapIos.swift +0 -932
- package/ios/RNIapQueue.swift +0 -35
- package/jest.config.js +0 -194
- package/src/__test__/iap.test.d.ts +0 -1
- package/src/__test__/iap.test.js +0 -59
- package/src/hooks/useIAP.d.ts +0 -21
- package/src/hooks/useIAP.js +0 -140
- package/src/hooks/withIAPContext.d.ts +0 -21
- package/src/hooks/withIAPContext.js +0 -142
- package/src/iap.d.ts +0 -197
- package/src/iap.js +0 -625
- package/src/index.d.ts +0 -4
- package/src/index.js +0 -4
- package/src/types/amazon.d.ts +0 -23
- package/src/types/amazon.js +0 -1
- package/src/types/android.d.ts +0 -47
- package/src/types/android.js +0 -22
- package/src/types/apple.d.ts +0 -424
- package/src/types/apple.js +0 -165
- package/src/types/index.d.ts +0 -117
- package/src/types/index.js +0 -40
- package/test/mocks/react-native-modules.js +0 -14
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
package expo.modules.iap
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import com.android.billingclient.api.AcknowledgePurchaseParams
|
|
6
|
+
import com.android.billingclient.api.BillingClient
|
|
7
|
+
import com.android.billingclient.api.BillingClientStateListener
|
|
8
|
+
import com.android.billingclient.api.BillingFlowParams
|
|
9
|
+
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams
|
|
10
|
+
import com.android.billingclient.api.BillingResult
|
|
11
|
+
import com.android.billingclient.api.ConsumeParams
|
|
12
|
+
import com.android.billingclient.api.ProductDetails
|
|
13
|
+
import com.android.billingclient.api.Purchase
|
|
14
|
+
import com.android.billingclient.api.PurchaseHistoryRecord
|
|
15
|
+
import com.android.billingclient.api.PurchasesUpdatedListener
|
|
16
|
+
import com.android.billingclient.api.QueryProductDetailsParams
|
|
17
|
+
import com.android.billingclient.api.QueryPurchaseHistoryParams
|
|
18
|
+
import com.android.billingclient.api.QueryPurchasesParams
|
|
19
|
+
import com.google.android.gms.common.ConnectionResult
|
|
20
|
+
import com.google.android.gms.common.GoogleApiAvailability
|
|
21
|
+
import expo.modules.kotlin.Promise
|
|
22
|
+
import expo.modules.kotlin.exception.Exceptions
|
|
23
|
+
import expo.modules.kotlin.modules.Module
|
|
24
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
25
|
+
|
|
26
|
+
class ExpoIapModule :
|
|
27
|
+
Module(),
|
|
28
|
+
PurchasesUpdatedListener {
|
|
29
|
+
companion object {
|
|
30
|
+
const val TAG = "ExpoIapModule"
|
|
31
|
+
const val E_NOT_PREPARED = "E_NOT_PREPARED"
|
|
32
|
+
const val E_INIT_CONNECTION = "E_INIT_CONNECTION"
|
|
33
|
+
const val E_QUERY_PRODUCT = "E_QUERY_PRODUCT"
|
|
34
|
+
const val EMPTY_SKU_LIST = "EMPTY_SKU_LIST"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
object IapEvent {
|
|
38
|
+
const val PURCHASE_UPDATED = "purchase-updated"
|
|
39
|
+
const val PURCHASE_ERROR = "purchase-error"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private var billingClientCache: BillingClient? = null
|
|
43
|
+
private val skus: MutableMap<String, ProductDetails> = mutableMapOf()
|
|
44
|
+
private val context: Context
|
|
45
|
+
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
46
|
+
private val currentActivity
|
|
47
|
+
get() =
|
|
48
|
+
appContext.activityProvider?.currentActivity
|
|
49
|
+
?: throw MissingCurrentActivityException()
|
|
50
|
+
|
|
51
|
+
override fun onPurchasesUpdated(
|
|
52
|
+
billingResult: BillingResult,
|
|
53
|
+
purchases: List<Purchase>?,
|
|
54
|
+
) {
|
|
55
|
+
val responseCode = billingResult.responseCode
|
|
56
|
+
if (responseCode != BillingClient.BillingResponseCode.OK) {
|
|
57
|
+
val error =
|
|
58
|
+
mutableMapOf<String, Any?>(
|
|
59
|
+
"responseCode" to responseCode,
|
|
60
|
+
"debugMessage" to billingResult.debugMessage,
|
|
61
|
+
)
|
|
62
|
+
val errorData = PlayUtils.getBillingResponseData(responseCode)
|
|
63
|
+
error["code"] = errorData.code
|
|
64
|
+
error["message"] = errorData.message
|
|
65
|
+
sendEvent(IapEvent.PURCHASE_ERROR, error.toMap())
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (purchases != null) {
|
|
70
|
+
val promiseItems = mutableListOf<Map<String, Any?>>()
|
|
71
|
+
purchases.forEach { purchase ->
|
|
72
|
+
val item =
|
|
73
|
+
mutableMapOf<String, Any?>(
|
|
74
|
+
"productId" to purchase.products[0],
|
|
75
|
+
"productIds" to purchase.products,
|
|
76
|
+
"transactionId" to purchase.orderId,
|
|
77
|
+
"transactionDate" to purchase.purchaseTime.toDouble(),
|
|
78
|
+
"transactionReceipt" to purchase.originalJson,
|
|
79
|
+
"purchaseToken" to purchase.purchaseToken,
|
|
80
|
+
"dataAndroid" to purchase.originalJson,
|
|
81
|
+
"signatureAndroid" to purchase.signature,
|
|
82
|
+
"autoRenewingAndroid" to purchase.isAutoRenewing,
|
|
83
|
+
"isAcknowledgedAndroid" to purchase.isAcknowledged,
|
|
84
|
+
"purchaseStateAndroid" to purchase.purchaseState,
|
|
85
|
+
"packageNameAndroid" to purchase.packageName,
|
|
86
|
+
"developerPayloadAndroid" to purchase.developerPayload,
|
|
87
|
+
)
|
|
88
|
+
purchase.accountIdentifiers?.let { accountIdentifiers ->
|
|
89
|
+
item["obfuscatedAccountIdAndroid"] = accountIdentifiers.obfuscatedAccountId
|
|
90
|
+
item["obfuscatedProfileIdAndroid"] = accountIdentifiers.obfuscatedProfileId
|
|
91
|
+
}
|
|
92
|
+
promiseItems.add(item.toMap())
|
|
93
|
+
sendEvent(IapEvent.PURCHASE_UPDATED, item.toMap())
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
val result =
|
|
97
|
+
mutableMapOf<String, Any?>(
|
|
98
|
+
"responseCode" to billingResult.responseCode,
|
|
99
|
+
"debugMessage" to billingResult.debugMessage,
|
|
100
|
+
"extraMessage" to
|
|
101
|
+
"The purchases are null. This is a normal behavior if you have requested DEFERRED proration. If not please report an issue.",
|
|
102
|
+
)
|
|
103
|
+
sendEvent(IapEvent.PURCHASE_UPDATED, result.toMap())
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
override fun definition() =
|
|
108
|
+
ModuleDefinition {
|
|
109
|
+
Name("ExpoIap")
|
|
110
|
+
|
|
111
|
+
Constants("PI" to Math.PI)
|
|
112
|
+
|
|
113
|
+
Events(IapEvent.PURCHASE_UPDATED, IapEvent.PURCHASE_ERROR)
|
|
114
|
+
|
|
115
|
+
AsyncFunction("initConnection") { promise: Promise ->
|
|
116
|
+
initBillingClient(promise) { promise.resolve(true) }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
AsyncFunction("endConnection") { promise: Promise ->
|
|
120
|
+
billingClientCache?.endConnection()
|
|
121
|
+
billingClientCache = null
|
|
122
|
+
skus.clear()
|
|
123
|
+
promise.resolve(true)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
AsyncFunction("getItemsByType") { type: String, skuArr: Array<String>, promise: Promise ->
|
|
127
|
+
ensureConnection(promise) { billingClient ->
|
|
128
|
+
val skuList =
|
|
129
|
+
skuArr.map { sku ->
|
|
130
|
+
QueryProductDetailsParams.Product
|
|
131
|
+
.newBuilder()
|
|
132
|
+
.setProductId(sku)
|
|
133
|
+
.setProductType(type)
|
|
134
|
+
.build()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (skuList.isEmpty()) {
|
|
138
|
+
promise.reject(EMPTY_SKU_LIST, "The SKU list is empty.", null)
|
|
139
|
+
return@ensureConnection
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
val params =
|
|
143
|
+
QueryProductDetailsParams
|
|
144
|
+
.newBuilder()
|
|
145
|
+
.setProductList(skuList)
|
|
146
|
+
.build()
|
|
147
|
+
|
|
148
|
+
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
|
|
149
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
150
|
+
promise.reject(
|
|
151
|
+
E_QUERY_PRODUCT,
|
|
152
|
+
"Error querying product details: ${billingResult.debugMessage}",
|
|
153
|
+
null,
|
|
154
|
+
)
|
|
155
|
+
return@queryProductDetailsAsync
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
val items =
|
|
159
|
+
productDetailsList.map { productDetails ->
|
|
160
|
+
skus[productDetails.productId] = productDetails
|
|
161
|
+
|
|
162
|
+
mapOf(
|
|
163
|
+
"productId" to productDetails.productId,
|
|
164
|
+
"title" to productDetails.title,
|
|
165
|
+
"description" to productDetails.description,
|
|
166
|
+
"productType" to productDetails.productType,
|
|
167
|
+
"name" to productDetails.name,
|
|
168
|
+
"oneTimePurchaseOfferDetails" to
|
|
169
|
+
productDetails.oneTimePurchaseOfferDetails?.let {
|
|
170
|
+
mapOf(
|
|
171
|
+
"priceCurrencyCode" to it.priceCurrencyCode,
|
|
172
|
+
"formattedPrice" to it.formattedPrice,
|
|
173
|
+
"priceAmountMicros" to it.priceAmountMicros.toString(),
|
|
174
|
+
)
|
|
175
|
+
},
|
|
176
|
+
"subscriptionOfferDetails" to
|
|
177
|
+
productDetails.subscriptionOfferDetails?.map { subscriptionOfferDetailsItem ->
|
|
178
|
+
mapOf(
|
|
179
|
+
"basePlanId" to subscriptionOfferDetailsItem.basePlanId,
|
|
180
|
+
"offerId" to subscriptionOfferDetailsItem.offerId,
|
|
181
|
+
"offerToken" to subscriptionOfferDetailsItem.offerToken,
|
|
182
|
+
"offerTags" to subscriptionOfferDetailsItem.offerTags,
|
|
183
|
+
"pricingPhases" to
|
|
184
|
+
mapOf(
|
|
185
|
+
"pricingPhaseList" to
|
|
186
|
+
subscriptionOfferDetailsItem.pricingPhases.pricingPhaseList.map
|
|
187
|
+
{ pricingPhaseItem ->
|
|
188
|
+
mapOf(
|
|
189
|
+
"formattedPrice" to pricingPhaseItem.formattedPrice,
|
|
190
|
+
"priceCurrencyCode" to pricingPhaseItem.priceCurrencyCode,
|
|
191
|
+
"billingPeriod" to pricingPhaseItem.billingPeriod,
|
|
192
|
+
"billingCycleCount" to pricingPhaseItem.billingCycleCount,
|
|
193
|
+
"priceAmountMicros" to
|
|
194
|
+
pricingPhaseItem.priceAmountMicros.toString(),
|
|
195
|
+
"recurrenceMode" to pricingPhaseItem.recurrenceMode,
|
|
196
|
+
)
|
|
197
|
+
},
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
promise.resolve(items)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
AsyncFunction("getAvailableItemsByType") { type: String, promise: Promise ->
|
|
209
|
+
ensureConnection(promise) { billingClient ->
|
|
210
|
+
val items = mutableListOf<Map<String, Any?>>()
|
|
211
|
+
billingClient.queryPurchasesAsync(
|
|
212
|
+
QueryPurchasesParams
|
|
213
|
+
.newBuilder()
|
|
214
|
+
.setProductType(
|
|
215
|
+
if (type == "subs") BillingClient.ProductType.SUBS else BillingClient.ProductType.INAPP,
|
|
216
|
+
).build(),
|
|
217
|
+
) { billingResult: BillingResult, purchases: List<Purchase>? ->
|
|
218
|
+
if (!isValidResult(billingResult, promise)) return@queryPurchasesAsync
|
|
219
|
+
purchases?.forEach { purchase ->
|
|
220
|
+
val item =
|
|
221
|
+
mutableMapOf<String, Any?>(
|
|
222
|
+
// kept for convenience/backward-compatibility. productIds has the complete list
|
|
223
|
+
"productId" to purchase.products[0],
|
|
224
|
+
"productIds" to purchase.products,
|
|
225
|
+
"transactionId" to purchase.orderId,
|
|
226
|
+
"transactionDate" to purchase.purchaseTime.toDouble(),
|
|
227
|
+
"transactionReceipt" to purchase.originalJson,
|
|
228
|
+
"orderId" to purchase.orderId,
|
|
229
|
+
"purchaseToken" to purchase.purchaseToken,
|
|
230
|
+
"developerPayloadAndroid" to purchase.developerPayload,
|
|
231
|
+
"signatureAndroid" to purchase.signature,
|
|
232
|
+
"purchaseStateAndroid" to purchase.purchaseState,
|
|
233
|
+
"isAcknowledgedAndroid" to purchase.isAcknowledged,
|
|
234
|
+
"packageNameAndroid" to purchase.packageName,
|
|
235
|
+
"obfuscatedAccountIdAndroid" to purchase.accountIdentifiers?.obfuscatedAccountId,
|
|
236
|
+
"obfuscatedProfileIdAndroid" to purchase.accountIdentifiers?.obfuscatedProfileId,
|
|
237
|
+
)
|
|
238
|
+
if (type == BillingClient.ProductType.SUBS) {
|
|
239
|
+
item["autoRenewingAndroid"] = purchase.isAutoRenewing
|
|
240
|
+
}
|
|
241
|
+
items.add(item)
|
|
242
|
+
}
|
|
243
|
+
promise.resolve(items)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
AsyncFunction("getPurchaseHistoryByType") { type: String, promise: Promise ->
|
|
249
|
+
ensureConnection(promise) { billingClient ->
|
|
250
|
+
billingClient.queryPurchaseHistoryAsync(
|
|
251
|
+
QueryPurchaseHistoryParams
|
|
252
|
+
.newBuilder()
|
|
253
|
+
.setProductType(
|
|
254
|
+
if (type == "subs") BillingClient.ProductType.SUBS else BillingClient.ProductType.INAPP,
|
|
255
|
+
).build(),
|
|
256
|
+
) { billingResult: BillingResult, purchaseHistoryRecordList: List<PurchaseHistoryRecord>? ->
|
|
257
|
+
|
|
258
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
259
|
+
PlayUtils.rejectPromiseWithBillingError(
|
|
260
|
+
promise,
|
|
261
|
+
billingResult.responseCode,
|
|
262
|
+
)
|
|
263
|
+
return@queryPurchaseHistoryAsync
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
Log.d(TAG, purchaseHistoryRecordList.toString())
|
|
267
|
+
val items = mutableListOf<Map<String, Any?>>()
|
|
268
|
+
purchaseHistoryRecordList?.forEach { purchase ->
|
|
269
|
+
val item =
|
|
270
|
+
mutableMapOf<String, Any?>(
|
|
271
|
+
"productId" to purchase.products[0],
|
|
272
|
+
"productIds" to purchase.products,
|
|
273
|
+
"transactionDate" to purchase.purchaseTime.toDouble(),
|
|
274
|
+
"transactionReceipt" to purchase.originalJson,
|
|
275
|
+
"purchaseToken" to purchase.purchaseToken,
|
|
276
|
+
"dataAndroid" to purchase.originalJson,
|
|
277
|
+
"signatureAndroid" to purchase.signature,
|
|
278
|
+
"developerPayload" to purchase.developerPayload,
|
|
279
|
+
)
|
|
280
|
+
items.add(item)
|
|
281
|
+
}
|
|
282
|
+
promise.resolve(items)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
AsyncFunction("buyItemByType") { params: Map<String, Any?>, promise: Promise ->
|
|
288
|
+
val type = params["type"] as String
|
|
289
|
+
val skuArr =
|
|
290
|
+
(params["skuArr"] as? List<*>)?.filterIsInstance<String>()?.toTypedArray()
|
|
291
|
+
?: emptyArray()
|
|
292
|
+
val purchaseToken = params["purchaseToken"] as? String
|
|
293
|
+
val replacementMode = (params["replacementMode"] as? Double)?.toInt() ?: -1
|
|
294
|
+
val obfuscatedAccountId = params["obfuscatedAccountId"] as? String
|
|
295
|
+
val obfuscatedProfileId = params["obfuscatedProfileId"] as? String
|
|
296
|
+
val offerTokenArr =
|
|
297
|
+
(params["offerTokenArr"] as? List<*>)
|
|
298
|
+
?.filterIsInstance<String>()
|
|
299
|
+
?.toTypedArray() ?: emptyArray()
|
|
300
|
+
val isOfferPersonalized = params["isOfferPersonalized"] as? Boolean ?: false
|
|
301
|
+
|
|
302
|
+
if (currentActivity == null) {
|
|
303
|
+
throw Exception("getCurrentActivity returned null")
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
ensureConnection(promise) { billingClient ->
|
|
307
|
+
if (type == BillingClient.ProductType.SUBS && skuArr.size != offerTokenArr.size) {
|
|
308
|
+
val debugMessage =
|
|
309
|
+
"The number of skus (${skuArr.size}) must match: the number of offerTokens (${offerTokenArr.size}) for Subscriptions"
|
|
310
|
+
sendEvent(
|
|
311
|
+
IapEvent.PURCHASE_ERROR,
|
|
312
|
+
mapOf(
|
|
313
|
+
"debugMessage" to debugMessage,
|
|
314
|
+
"code" to "E_SKU_OFFER_MISMATCH",
|
|
315
|
+
"message" to debugMessage,
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
throw Exception(debugMessage)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
val productParamsList =
|
|
322
|
+
skuArr.mapIndexed { index, sku ->
|
|
323
|
+
val selectedSku = skus[sku]
|
|
324
|
+
if (selectedSku == null) {
|
|
325
|
+
val debugMessage =
|
|
326
|
+
"The sku was not found. Please fetch products first by calling getItems"
|
|
327
|
+
sendEvent(
|
|
328
|
+
IapEvent.PURCHASE_ERROR,
|
|
329
|
+
mapOf(
|
|
330
|
+
"debugMessage" to debugMessage,
|
|
331
|
+
"code" to "E_SKU_NOT_FOUND",
|
|
332
|
+
"message" to debugMessage,
|
|
333
|
+
"productId" to sku,
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
throw Exception(debugMessage)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
val productDetailParams =
|
|
340
|
+
BillingFlowParams.ProductDetailsParams
|
|
341
|
+
.newBuilder()
|
|
342
|
+
.setProductDetails(selectedSku)
|
|
343
|
+
|
|
344
|
+
if (type == BillingClient.ProductType.SUBS) {
|
|
345
|
+
productDetailParams.setOfferToken(offerTokenArr[index])
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
productDetailParams.build()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
val builder =
|
|
352
|
+
BillingFlowParams
|
|
353
|
+
.newBuilder()
|
|
354
|
+
.setProductDetailsParamsList(productParamsList)
|
|
355
|
+
.setIsOfferPersonalized(isOfferPersonalized)
|
|
356
|
+
|
|
357
|
+
if (purchaseToken != null) {
|
|
358
|
+
val subscriptionUpdateParams =
|
|
359
|
+
SubscriptionUpdateParams
|
|
360
|
+
.newBuilder()
|
|
361
|
+
.setOldPurchaseToken(purchaseToken)
|
|
362
|
+
|
|
363
|
+
if (type == BillingClient.ProductType.SUBS && replacementMode != -1) {
|
|
364
|
+
val mode =
|
|
365
|
+
when (replacementMode) {
|
|
366
|
+
SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE ->
|
|
367
|
+
SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE
|
|
368
|
+
SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION ->
|
|
369
|
+
SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION
|
|
370
|
+
SubscriptionUpdateParams.ReplacementMode.DEFERRED ->
|
|
371
|
+
SubscriptionUpdateParams.ReplacementMode.DEFERRED
|
|
372
|
+
SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION ->
|
|
373
|
+
SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION
|
|
374
|
+
SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE ->
|
|
375
|
+
SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE
|
|
376
|
+
else -> SubscriptionUpdateParams.ReplacementMode.UNKNOWN_REPLACEMENT_MODE
|
|
377
|
+
}
|
|
378
|
+
subscriptionUpdateParams.setSubscriptionReplacementMode(mode)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
builder.setSubscriptionUpdateParams(subscriptionUpdateParams.build())
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
obfuscatedAccountId?.let { builder.setObfuscatedAccountId(it) }
|
|
385
|
+
obfuscatedProfileId?.let { builder.setObfuscatedProfileId(it) }
|
|
386
|
+
|
|
387
|
+
val flowParams = builder.build()
|
|
388
|
+
val billingResult = billingClient.launchBillingFlow(currentActivity, flowParams)
|
|
389
|
+
|
|
390
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
391
|
+
promise.reject(
|
|
392
|
+
"Billing Error",
|
|
393
|
+
billingResult.debugMessage,
|
|
394
|
+
null,
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
promise.resolve(true)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
AsyncFunction("acknowledgePurchase") {
|
|
403
|
+
token: String,
|
|
404
|
+
promise: Promise,
|
|
405
|
+
->
|
|
406
|
+
|
|
407
|
+
ensureConnection(promise) { billingClient ->
|
|
408
|
+
val acknowledgePurchaseParams =
|
|
409
|
+
AcknowledgePurchaseParams
|
|
410
|
+
.newBuilder()
|
|
411
|
+
.setPurchaseToken(token)
|
|
412
|
+
.build()
|
|
413
|
+
|
|
414
|
+
billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult: BillingResult ->
|
|
415
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
416
|
+
PlayUtils.rejectPromiseWithBillingError(
|
|
417
|
+
promise,
|
|
418
|
+
billingResult.responseCode,
|
|
419
|
+
)
|
|
420
|
+
return@acknowledgePurchase
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
val map = mutableMapOf<String, Any?>()
|
|
424
|
+
map["responseCode"] = billingResult.responseCode
|
|
425
|
+
map["debugMessage"] = billingResult.debugMessage
|
|
426
|
+
val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
|
|
427
|
+
map["code"] = errorData.code
|
|
428
|
+
map["message"] = errorData.message
|
|
429
|
+
promise.resolve(map)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
AsyncFunction("consumeProduct") {
|
|
435
|
+
token: String,
|
|
436
|
+
promise: Promise,
|
|
437
|
+
->
|
|
438
|
+
|
|
439
|
+
val params = ConsumeParams.newBuilder().setPurchaseToken(token).build()
|
|
440
|
+
|
|
441
|
+
ensureConnection(promise) { billingClient ->
|
|
442
|
+
billingClient.consumeAsync(params) { billingResult: BillingResult, purchaseToken: String? ->
|
|
443
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
444
|
+
PlayUtils.rejectPromiseWithBillingError(
|
|
445
|
+
promise,
|
|
446
|
+
billingResult.responseCode,
|
|
447
|
+
)
|
|
448
|
+
return@consumeAsync
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
val map = mutableMapOf<String, Any?>()
|
|
452
|
+
map["responseCode"] = billingResult.responseCode
|
|
453
|
+
map["debugMessage"] = billingResult.debugMessage
|
|
454
|
+
val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
|
|
455
|
+
map["code"] = errorData.code
|
|
456
|
+
map["message"] = errorData.message
|
|
457
|
+
map["purchaseToken"] = purchaseToken
|
|
458
|
+
promise.resolve(map)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Rejects promise with billing code if BillingResult is not OK
|
|
466
|
+
*/
|
|
467
|
+
private fun isValidResult(
|
|
468
|
+
billingResult: BillingResult,
|
|
469
|
+
promise: Promise,
|
|
470
|
+
): Boolean {
|
|
471
|
+
Log.d(TAG, "responseCode: " + billingResult.responseCode)
|
|
472
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
473
|
+
PlayUtils.rejectPromiseWithBillingError(promise, billingResult.responseCode)
|
|
474
|
+
return false
|
|
475
|
+
}
|
|
476
|
+
return true
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private fun ensureConnection(
|
|
480
|
+
promise: Promise,
|
|
481
|
+
callback: (billingClient: BillingClient) -> Unit,
|
|
482
|
+
) {
|
|
483
|
+
if (billingClientCache?.isReady == true) {
|
|
484
|
+
callback(billingClientCache!!)
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
initBillingClient(promise, callback)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private fun initBillingClient(
|
|
492
|
+
promise: Promise,
|
|
493
|
+
callback: (billingClient: BillingClient) -> Unit,
|
|
494
|
+
) {
|
|
495
|
+
if (GoogleApiAvailability
|
|
496
|
+
.getInstance()
|
|
497
|
+
.isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS
|
|
498
|
+
) {
|
|
499
|
+
Log.i(TAG, "Google Play Services are not available on this device")
|
|
500
|
+
promise.reject(
|
|
501
|
+
E_NOT_PREPARED,
|
|
502
|
+
"Google Play Services are not available on this device",
|
|
503
|
+
null,
|
|
504
|
+
)
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
billingClientCache =
|
|
509
|
+
BillingClient
|
|
510
|
+
.newBuilder(context)
|
|
511
|
+
.setListener(this)
|
|
512
|
+
.enablePendingPurchases()
|
|
513
|
+
.build()
|
|
514
|
+
|
|
515
|
+
billingClientCache?.startConnection(
|
|
516
|
+
object : BillingClientStateListener {
|
|
517
|
+
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
|
518
|
+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
519
|
+
promise.reject(
|
|
520
|
+
E_INIT_CONNECTION,
|
|
521
|
+
"Billing setup finished with error: ${billingResult.debugMessage}",
|
|
522
|
+
null,
|
|
523
|
+
)
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
callback(billingClientCache!!)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
override fun onBillingServiceDisconnected() {
|
|
530
|
+
Log.i(TAG, "Billing service disconnected")
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
package expo.modules.iap
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.android.billingclient.api.BillingClient
|
|
5
|
+
import expo.modules.kotlin.Promise
|
|
6
|
+
|
|
7
|
+
data class BillingResponse(
|
|
8
|
+
val code: String,
|
|
9
|
+
val message: String,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
object PromiseUtils {
|
|
13
|
+
private val promises = HashMap<String, MutableList<Promise>>()
|
|
14
|
+
|
|
15
|
+
const val TAG = "PromiseUtils"
|
|
16
|
+
const val E_ACTIVITY_UNAVAILABLE = "E_ACTIVITY_UNAVAILABLE"
|
|
17
|
+
const val E_UNKNOWN = "E_UNKNOWN"
|
|
18
|
+
const val E_NOT_PREPARED = "E_NOT_PREPARED"
|
|
19
|
+
const val E_ALREADY_PREPARED = "E_ALREADY_PREPARED"
|
|
20
|
+
const val E_PENDING = "E_PENDING"
|
|
21
|
+
const val E_NOT_ENDED = "E_NOT_ENDED"
|
|
22
|
+
const val E_USER_CANCELLED = "E_USER_CANCELLED"
|
|
23
|
+
const val E_ITEM_UNAVAILABLE = "E_ITEM_UNAVAILABLE"
|
|
24
|
+
const val E_NETWORK_ERROR = "E_NETWORK_ERROR"
|
|
25
|
+
const val E_SERVICE_ERROR = "E_SERVICE_ERROR"
|
|
26
|
+
const val E_ALREADY_OWNED = "E_ALREADY_OWNED"
|
|
27
|
+
const val E_REMOTE_ERROR = "E_REMOTE_ERROR"
|
|
28
|
+
const val E_USER_ERROR = "E_USER_ERROR"
|
|
29
|
+
const val E_DEVELOPER_ERROR = "E_DEVELOPER_ERROR"
|
|
30
|
+
const val E_BILLING_RESPONSE_JSON_PARSE_ERROR = "E_BILLING_RESPONSE_JSON_PARSE_ERROR"
|
|
31
|
+
const val E_CONNECTION_CLOSED = "E_CONNECTION_CLOSED"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
object PlayUtils {
|
|
35
|
+
const val TAG = "PlayUtils"
|
|
36
|
+
|
|
37
|
+
fun rejectPromiseWithBillingError(
|
|
38
|
+
promise: Promise,
|
|
39
|
+
responseCode: Int,
|
|
40
|
+
) {
|
|
41
|
+
val errorData = getBillingResponseData(responseCode)
|
|
42
|
+
promise.reject(errorData.code, errorData.message, null)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun getBillingResponseData(responseCode: Int): BillingResponse {
|
|
46
|
+
val errorData =
|
|
47
|
+
when (responseCode) {
|
|
48
|
+
BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> {
|
|
49
|
+
BillingResponse(
|
|
50
|
+
PromiseUtils.E_SERVICE_ERROR,
|
|
51
|
+
"This feature is not available on your device.",
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> {
|
|
55
|
+
BillingResponse(
|
|
56
|
+
PromiseUtils.E_NETWORK_ERROR,
|
|
57
|
+
"The service is disconnected (check your internet connection.)",
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
BillingClient.BillingResponseCode.NETWORK_ERROR -> {
|
|
61
|
+
BillingResponse(
|
|
62
|
+
PromiseUtils.E_NETWORK_ERROR,
|
|
63
|
+
"You have a problem with network connection.",
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
BillingClient.BillingResponseCode.OK -> {
|
|
67
|
+
BillingResponse(
|
|
68
|
+
"OK",
|
|
69
|
+
"",
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
BillingClient.BillingResponseCode.USER_CANCELED -> {
|
|
73
|
+
BillingResponse(
|
|
74
|
+
PromiseUtils.E_USER_CANCELLED,
|
|
75
|
+
"Payment is cancelled.",
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> {
|
|
79
|
+
BillingResponse(
|
|
80
|
+
PromiseUtils.E_SERVICE_ERROR,
|
|
81
|
+
"The service is unreachable. This may be your internet connection, or the Play Store may be down.",
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> {
|
|
85
|
+
BillingResponse(
|
|
86
|
+
PromiseUtils.E_SERVICE_ERROR,
|
|
87
|
+
"Billing is unavailable. This may be a problem with your device, or the Play Store may be down.",
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> {
|
|
91
|
+
BillingResponse(
|
|
92
|
+
PromiseUtils.E_ITEM_UNAVAILABLE,
|
|
93
|
+
"That item is unavailable.",
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
BillingClient.BillingResponseCode.DEVELOPER_ERROR -> {
|
|
97
|
+
BillingResponse(
|
|
98
|
+
PromiseUtils.E_DEVELOPER_ERROR,
|
|
99
|
+
"Google is indicating that we have some issue connecting to payment.",
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
BillingClient.BillingResponseCode.ERROR -> {
|
|
103
|
+
BillingResponse(
|
|
104
|
+
PromiseUtils.E_UNKNOWN,
|
|
105
|
+
"An unknown or unexpected error has occurred. Please try again later.",
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
|
|
109
|
+
BillingResponse(
|
|
110
|
+
PromiseUtils.E_ALREADY_OWNED,
|
|
111
|
+
"You already own this item.",
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
else -> {
|
|
115
|
+
BillingResponse(
|
|
116
|
+
PromiseUtils.E_UNKNOWN,
|
|
117
|
+
"Purchase failed with code: $responseCode",
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
Log.e(TAG, "Error Code: $responseCode")
|
|
122
|
+
return errorData
|
|
123
|
+
}
|
|
124
|
+
}
|