expo-iap 2.2.4-rc.2 → 2.2.5-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/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +17 -14
- package/android/src/main/java/expo/modules/iap/PlayUtils.kt +70 -0
- package/build/useIap.d.ts.map +1 -1
- package/build/useIap.js +3 -4
- package/build/useIap.js.map +1 -1
- package/iap.md +27 -5
- package/ios/ExpoIapModule.swift +20 -41
- package/ios/Types.swift +0 -1
- package/package.json +1 -1
- package/src/useIap.ts +2 -4
|
@@ -32,6 +32,7 @@ class ExpoIapModule :
|
|
|
32
32
|
const val E_INIT_CONNECTION = "E_INIT_CONNECTION"
|
|
33
33
|
const val E_QUERY_PRODUCT = "E_QUERY_PRODUCT"
|
|
34
34
|
const val EMPTY_SKU_LIST = "EMPTY_SKU_LIST"
|
|
35
|
+
private const val PROMISE_BUY_ITEM = "PROMISE_BUY_ITEM"
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
object IapEvent {
|
|
@@ -63,6 +64,7 @@ class ExpoIapModule :
|
|
|
63
64
|
error["code"] = errorData.code
|
|
64
65
|
error["message"] = errorData.message
|
|
65
66
|
sendEvent(IapEvent.PURCHASE_ERROR, error.toMap())
|
|
67
|
+
PromiseUtils.rejectPromisesWithBillingError(PROMISE_BUY_ITEM, responseCode)
|
|
66
68
|
return
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -92,6 +94,7 @@ class ExpoIapModule :
|
|
|
92
94
|
promiseItems.add(item.toMap())
|
|
93
95
|
sendEvent(IapEvent.PURCHASE_UPDATED, item.toMap())
|
|
94
96
|
}
|
|
97
|
+
PromiseUtils.resolvePromisesForKey(PROMISE_BUY_ITEM, promiseItems)
|
|
95
98
|
} else {
|
|
96
99
|
val result =
|
|
97
100
|
mutableMapOf<String, Any?>(
|
|
@@ -101,6 +104,7 @@ class ExpoIapModule :
|
|
|
101
104
|
"The purchases are null. This is a normal behavior if you have requested DEFERRED proration. If not please report an issue.",
|
|
102
105
|
)
|
|
103
106
|
sendEvent(IapEvent.PURCHASE_UPDATED, result.toMap())
|
|
107
|
+
PromiseUtils.resolvePromisesForKey(PROMISE_BUY_ITEM, result)
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -301,22 +305,25 @@ class ExpoIapModule :
|
|
|
301
305
|
val isOfferPersonalized = params["isOfferPersonalized"] as? Boolean ?: false
|
|
302
306
|
|
|
303
307
|
if (currentActivity == null) {
|
|
304
|
-
|
|
308
|
+
promise.reject("E_UNKNOWN", "getCurrentActivity returned null", null)
|
|
309
|
+
return@AsyncFunction
|
|
305
310
|
}
|
|
306
311
|
|
|
307
312
|
ensureConnection(promise) { billingClient ->
|
|
313
|
+
PromiseUtils.addPromiseForKey(PROMISE_BUY_ITEM, promise)
|
|
314
|
+
|
|
308
315
|
if (type == BillingClient.ProductType.SUBS && skuArr.size != offerTokenArr.size) {
|
|
309
|
-
val debugMessage =
|
|
310
|
-
"The number of skus (${skuArr.size}) must match: the number of offerTokens (${offerTokenArr.size}) for Subscriptions"
|
|
316
|
+
val debugMessage = "The number of skus (${skuArr.size}) must match: the number of offerTokens (${offerTokenArr.size}) for Subscriptions"
|
|
311
317
|
sendEvent(
|
|
312
318
|
IapEvent.PURCHASE_ERROR,
|
|
313
319
|
mapOf(
|
|
314
320
|
"debugMessage" to debugMessage,
|
|
315
321
|
"code" to "E_SKU_OFFER_MISMATCH",
|
|
316
322
|
"message" to debugMessage,
|
|
317
|
-
)
|
|
323
|
+
)
|
|
318
324
|
)
|
|
319
|
-
|
|
325
|
+
promise.reject("E_SKU_OFFER_MISMATCH", debugMessage, null)
|
|
326
|
+
return@ensureConnection
|
|
320
327
|
}
|
|
321
328
|
|
|
322
329
|
val productParamsList =
|
|
@@ -334,7 +341,8 @@ class ExpoIapModule :
|
|
|
334
341
|
"productId" to sku,
|
|
335
342
|
),
|
|
336
343
|
)
|
|
337
|
-
|
|
344
|
+
promise.reject("E_SKU_NOT_FOUND", debugMessage, null)
|
|
345
|
+
return@ensureConnection
|
|
338
346
|
}
|
|
339
347
|
|
|
340
348
|
val productDetailParams =
|
|
@@ -378,7 +386,6 @@ class ExpoIapModule :
|
|
|
378
386
|
}
|
|
379
387
|
subscriptionUpdateParams.setSubscriptionReplacementMode(mode)
|
|
380
388
|
}
|
|
381
|
-
|
|
382
389
|
builder.setSubscriptionUpdateParams(subscriptionUpdateParams.build())
|
|
383
390
|
}
|
|
384
391
|
|
|
@@ -389,14 +396,10 @@ class ExpoIapModule :
|
|
|
389
396
|
val billingResult = billingClient.launchBillingFlow(currentActivity, flowParams)
|
|
390
397
|
|
|
391
398
|
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
null,
|
|
396
|
-
)
|
|
399
|
+
val errorData = PlayUtils.getBillingResponseData(billingResult.responseCode)
|
|
400
|
+
promise.reject(errorData.code, billingResult.debugMessage, null)
|
|
401
|
+
return@ensureConnection
|
|
397
402
|
}
|
|
398
|
-
|
|
399
|
-
promise.resolve(true)
|
|
400
403
|
}
|
|
401
404
|
}
|
|
402
405
|
|
|
@@ -29,6 +29,76 @@ object PromiseUtils {
|
|
|
29
29
|
const val E_DEVELOPER_ERROR = "E_DEVELOPER_ERROR"
|
|
30
30
|
const val E_BILLING_RESPONSE_JSON_PARSE_ERROR = "E_BILLING_RESPONSE_JSON_PARSE_ERROR"
|
|
31
31
|
const val E_CONNECTION_CLOSED = "E_CONNECTION_CLOSED"
|
|
32
|
+
|
|
33
|
+
fun addPromiseForKey(
|
|
34
|
+
key: String,
|
|
35
|
+
promise: Promise,
|
|
36
|
+
) {
|
|
37
|
+
promises.getOrPut(key) { mutableListOf() }.add(promise)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fun resolvePromisesForKey(
|
|
41
|
+
key: String,
|
|
42
|
+
value: Any?,
|
|
43
|
+
) {
|
|
44
|
+
promises[key]?.forEach { promise ->
|
|
45
|
+
promise.safeResolve(value)
|
|
46
|
+
}
|
|
47
|
+
promises.remove(key)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fun rejectAllPendingPromises() {
|
|
51
|
+
promises.flatMap { it.value }.forEach { promise ->
|
|
52
|
+
promise.safeReject(E_CONNECTION_CLOSED, "Connection has been closed", null)
|
|
53
|
+
}
|
|
54
|
+
promises.clear()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fun rejectPromisesForKey(
|
|
58
|
+
key: String,
|
|
59
|
+
code: String,
|
|
60
|
+
message: String?,
|
|
61
|
+
err: Exception?,
|
|
62
|
+
) {
|
|
63
|
+
promises[key]?.forEach { promise ->
|
|
64
|
+
promise.safeReject(code, message, err)
|
|
65
|
+
}
|
|
66
|
+
promises.remove(key)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const val TAG = "IapPromises"
|
|
71
|
+
|
|
72
|
+
fun Promise.safeResolve(value: Any?) {
|
|
73
|
+
try {
|
|
74
|
+
this.resolve(value)
|
|
75
|
+
} catch (e: RuntimeException) {
|
|
76
|
+
Log.d(TAG, "Already consumed ${e.message}")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fun Promise.safeReject(message: String) = this.safeReject(message, null, null)
|
|
81
|
+
|
|
82
|
+
fun Promise.safeReject(
|
|
83
|
+
code: String,
|
|
84
|
+
message: String?,
|
|
85
|
+
) = this.safeReject(code, message, null)
|
|
86
|
+
|
|
87
|
+
fun Promise.safeReject(
|
|
88
|
+
code: String,
|
|
89
|
+
throwable: Throwable?,
|
|
90
|
+
) = this.safeReject(code, null, throwable)
|
|
91
|
+
|
|
92
|
+
fun Promise.safeReject(
|
|
93
|
+
code: String,
|
|
94
|
+
message: String?,
|
|
95
|
+
throwable: Throwable?,
|
|
96
|
+
) {
|
|
97
|
+
try {
|
|
98
|
+
this.reject(code, message, throwable)
|
|
99
|
+
} catch (e: RuntimeException) {
|
|
100
|
+
Log.d(TAG, "Already consumed ${e.message}")
|
|
101
|
+
}
|
|
32
102
|
}
|
|
33
103
|
|
|
34
104
|
object PlayUtils {
|
package/build/useIap.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useIap.d.ts","sourceRoot":"","sources":["../src/useIap.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useIap.d.ts","sourceRoot":"","sources":["../src/useIap.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,OAAO,EACP,eAAe,EACf,QAAQ,EACR,aAAa,EACb,cAAc,EACd,mBAAmB,EAEpB,MAAM,iBAAiB,CAAC;AAKzB,KAAK,UAAU,GAAG;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,mBAAmB,EAAE,eAAe,EAAE,CAAC;IACvC,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,iBAAiB,EAAE,eAAe,EAAE,CAAC;IACrC,kBAAkB,EAAE,eAAe,EAAE,CAAC;IACtC,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,oBAAoB,CAAC,EAAE,aAAa,CAAC;IACrC,iBAAiB,EAAE,CAAC,EAClB,QAAQ,EACR,YAAY,EACZ,uBAAuB,GACxB,EAAE;QACD,QAAQ,EAAE,QAAQ,CAAC;QACnB,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,uBAAuB,CAAC,EAAE,MAAM,CAAC;KAClC,KAAK,OAAO,CAAC,MAAM,GAAG,OAAO,GAAG,cAAc,GAAG,IAAI,CAAC,CAAC;IACxD,qBAAqB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,oBAAoB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD,CAAC;AAEF,wBAAgB,MAAM,IAAI,UAAU,CAmInC"}
|
package/build/useIap.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { endConnection, initConnection, purchaseErrorListener, purchaseUpdatedListener, transactionUpdatedIos, getProducts, getAvailablePurchases, getPurchaseHistory, getSubscriptions, } from './';
|
|
1
|
+
import { endConnection, initConnection, purchaseErrorListener, purchaseUpdatedListener, transactionUpdatedIos, getProducts, getAvailablePurchases, getPurchaseHistory, finishTransaction as finishTransactionInternal, getSubscriptions, } from './';
|
|
2
2
|
import { useCallback, useEffect, useState, useRef } from 'react';
|
|
3
3
|
import { Platform } from 'react-native';
|
|
4
4
|
export function useIAP() {
|
|
@@ -24,12 +24,11 @@ export function useIAP() {
|
|
|
24
24
|
const requestPurchaseHistories = useCallback(async () => {
|
|
25
25
|
setPurchaseHistories(await getPurchaseHistory());
|
|
26
26
|
}, []);
|
|
27
|
-
const finishTransaction = useCallback(async ({ purchase, isConsumable,
|
|
27
|
+
const finishTransaction = useCallback(async ({ purchase, isConsumable, }) => {
|
|
28
28
|
try {
|
|
29
|
-
return await
|
|
29
|
+
return await finishTransactionInternal({
|
|
30
30
|
purchase,
|
|
31
31
|
isConsumable,
|
|
32
|
-
developerPayloadAndroid,
|
|
33
32
|
});
|
|
34
33
|
}
|
|
35
34
|
catch (err) {
|
package/build/useIap.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useIap.js","sourceRoot":"","sources":["../src/useIap.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,WAAW,EACX,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,IAAI,CAAC;AACZ,OAAO,EAAC,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAC,MAAM,OAAO,CAAC;AAY/D,OAAO,EAAC,QAAQ,EAAC,MAAM,cAAc,CAAC;AA0BtC,MAAM,UAAU,MAAM;IACpB,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAU,KAAK,CAAC,CAAC;IAC3D,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAY,EAAE,CAAC,CAAC;IACxD,MAAM,CAAC,mBAAmB,EAAE,sBAAsB,CAAC,GAAG,QAAQ,CAE5D,EAAE,CAAC,CAAC;IACN,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAwB,EAAE,CAAC,CAAC;IAC9E,MAAM,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAG,QAAQ,CACxD,EAAE,CACH,CAAC;IACF,MAAM,CAAC,kBAAkB,EAAE,qBAAqB,CAAC,GAAG,QAAQ,CAE1D,EAAE,CAAC,CAAC;IACN,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,EAAmB,CAAC;IAC1E,MAAM,CAAC,oBAAoB,EAAE,uBAAuB,CAAC,GACnD,QAAQ,EAAiB,CAAC;IAE5B,2BAA2B;IAC3B,MAAM,gBAAgB,GAAG,MAAM,CAI5B,EAAE,CAAC,CAAC;IAEP,MAAM,eAAe,GAAG,WAAW,CAAC,KAAK,EAAE,IAAc,EAAiB,EAAE;QAC1E,WAAW,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IACvC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,oBAAoB,GAAG,WAAW,CACtC,KAAK,EAAE,IAAc,EAAiB,EAAE;QACtC,gBAAgB,CAAC,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;IACjD,CAAC,EACD,EAAE,CACH,CAAC;IAEF,MAAM,yBAAyB,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QACtE,qBAAqB,CAAC,MAAM,qBAAqB,EAAE,CAAC,CAAC;IACvD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,wBAAwB,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QACrE,oBAAoB,CAAC,MAAM,kBAAkB,EAAE,CAAC,CAAC;IACnD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,iBAAiB,GAAG,WAAW,CACnC,KAAK,EAAE,EACL,QAAQ,EACR,YAAY,
|
|
1
|
+
{"version":3,"file":"useIap.js","sourceRoot":"","sources":["../src/useIap.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n endConnection,\n initConnection,\n purchaseErrorListener,\n purchaseUpdatedListener,\n transactionUpdatedIos,\n getProducts,\n getAvailablePurchases,\n getPurchaseHistory,\n finishTransaction as finishTransactionInternal,\n getSubscriptions,\n} from './';\nimport {useCallback, useEffect, useState, useRef} from 'react';\nimport {\n Product,\n ProductPurchase,\n Purchase,\n PurchaseError,\n PurchaseResult,\n SubscriptionProduct,\n SubscriptionPurchase,\n} from './ExpoIap.types';\nimport {TransactionEvent} from './modules/ios';\nimport {Subscription} from 'expo-modules-core';\nimport {Platform} from 'react-native';\n\ntype IAP_STATUS = {\n connected: boolean;\n products: Product[];\n promotedProductsIOS: ProductPurchase[];\n subscriptions: SubscriptionProduct[];\n purchaseHistories: ProductPurchase[];\n availablePurchases: ProductPurchase[];\n currentPurchase?: ProductPurchase;\n currentPurchaseError?: PurchaseError;\n finishTransaction: ({\n purchase,\n isConsumable,\n developerPayloadAndroid,\n }: {\n purchase: Purchase;\n isConsumable?: boolean;\n developerPayloadAndroid?: string;\n }) => Promise<string | boolean | PurchaseResult | void>;\n getAvailablePurchases: () => Promise<void>;\n getPurchaseHistories: () => Promise<void>;\n getProducts: (skus: string[]) => Promise<void>;\n getSubscriptions: (skus: string[]) => Promise<void>;\n};\n\nexport function useIAP(): IAP_STATUS {\n const [connected, setConnected] = useState<boolean>(false);\n const [products, setProducts] = useState<Product[]>([]);\n const [promotedProductsIOS, setPromotedProductsIOS] = useState<\n ProductPurchase[]\n >([]);\n const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([]);\n const [purchaseHistories, setPurchaseHistories] = useState<ProductPurchase[]>(\n [],\n );\n const [availablePurchases, setAvailablePurchases] = useState<\n ProductPurchase[]\n >([]);\n const [currentPurchase, setCurrentPurchase] = useState<ProductPurchase>();\n const [currentPurchaseError, setCurrentPurchaseError] =\n useState<PurchaseError>();\n\n // 구독을 훅 인스턴스별로 관리하기 위한 ref\n const subscriptionsRef = useRef<{\n purchaseUpdate?: Subscription;\n purchaseError?: Subscription;\n promotedProductsIos?: Subscription;\n }>({});\n\n const requestProducts = useCallback(async (skus: string[]): Promise<void> => {\n setProducts(await getProducts(skus));\n }, []);\n\n const requestSubscriptions = useCallback(\n async (skus: string[]): Promise<void> => {\n setSubscriptions(await getSubscriptions(skus));\n },\n [],\n );\n\n const requestAvailablePurchases = useCallback(async (): Promise<void> => {\n setAvailablePurchases(await getAvailablePurchases());\n }, []);\n\n const requestPurchaseHistories = useCallback(async (): Promise<void> => {\n setPurchaseHistories(await getPurchaseHistory());\n }, []);\n\n const finishTransaction = useCallback(\n async ({\n purchase,\n isConsumable,\n }: {\n purchase: ProductPurchase;\n isConsumable?: boolean;\n }): Promise<string | boolean | PurchaseResult | void> => {\n try {\n return await finishTransactionInternal({\n purchase,\n isConsumable,\n });\n } catch (err) {\n throw err;\n } finally {\n if (purchase.id === currentPurchase?.id) {\n setCurrentPurchase(undefined);\n }\n if (purchase.id === currentPurchaseError?.productId) {\n setCurrentPurchaseError(undefined);\n }\n }\n },\n [currentPurchase?.id, currentPurchaseError?.productId],\n );\n\n const initIapWithSubscriptions = useCallback(async (): Promise<void> => {\n const result = await initConnection();\n setConnected(result);\n\n if (result) {\n subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(\n async (purchase: Purchase | SubscriptionPurchase) => {\n setCurrentPurchaseError(undefined);\n setCurrentPurchase(purchase);\n },\n );\n\n subscriptionsRef.current.purchaseError = purchaseErrorListener(\n (error: PurchaseError) => {\n setCurrentPurchase(undefined);\n setCurrentPurchaseError(error);\n },\n );\n\n if (Platform.OS === 'ios') {\n subscriptionsRef.current.promotedProductsIos = transactionUpdatedIos(\n (event: TransactionEvent) => {\n setPromotedProductsIOS((prevProducts) =>\n event.transaction\n ? [...prevProducts, event.transaction]\n : prevProducts,\n );\n },\n );\n }\n }\n }, []);\n\n useEffect(() => {\n initIapWithSubscriptions();\n const currentSubscriptions = subscriptionsRef.current;\n\n return () => {\n currentSubscriptions.purchaseUpdate?.remove();\n currentSubscriptions.purchaseError?.remove();\n currentSubscriptions.promotedProductsIos?.remove();\n endConnection();\n setConnected(false);\n };\n }, [initIapWithSubscriptions]);\n\n return {\n connected,\n products,\n promotedProductsIOS,\n subscriptions,\n purchaseHistories,\n finishTransaction,\n availablePurchases,\n currentPurchase,\n currentPurchaseError,\n getProducts: requestProducts,\n getSubscriptions: requestSubscriptions,\n getAvailablePurchases: requestAvailablePurchases,\n getPurchaseHistories: requestPurchaseHistories,\n };\n}\n"]}
|
package/iap.md
CHANGED
|
@@ -142,17 +142,39 @@ This section describes purchase properties in `expo-iap`.
|
|
|
142
142
|
| `id` | `string` | Purchased product ID |
|
|
143
143
|
| `transactionId` | `string?` | Transaction ID (optional) |
|
|
144
144
|
| `transactionDate` | `number` | Unix timestamp |
|
|
145
|
-
| `
|
|
145
|
+
| `transactionReceipt` | `string` | Receipt data |
|
|
146
146
|
|
|
147
147
|
### Android-Only Purchase Types
|
|
148
148
|
|
|
149
|
-
- **`ProductPurchase`**:
|
|
150
|
-
-
|
|
149
|
+
- **`ProductPurchase`**:
|
|
150
|
+
- Adds the following properties specific to in-app product purchases:
|
|
151
|
+
- **`ids`**: `string[]` - A list of product IDs associated with the purchase (for multi-item purchases).
|
|
152
|
+
- **`dataAndroid`**: `string` - The raw purchase data from Google Play (e.g., JSON payload).
|
|
153
|
+
- **`signatureAndroid`**: `string` - The cryptographic signature from Google Play to verify the purchase's authenticity.
|
|
154
|
+
- **`purchaseStateAndroid`**: `number` - The state of the purchase (e.g., 0 = purchased, 1 = canceled, 2 = pending).
|
|
155
|
+
|
|
156
|
+
- **`SubscriptionPurchase`**:
|
|
157
|
+
- Extends the base properties and includes:
|
|
158
|
+
- **`autoRenewingAndroid`**: `boolean` - Indicates whether the subscription automatically renews (true) or not (false).
|
|
159
|
+
|
|
160
|
+
- **`purchaseTokenAndroid`**:
|
|
161
|
+
- **Description**: A unique identifier provided by Google Play for each purchase, used to track, verify, and manage the transaction. For example, it is required to "consume" a consumable product (e.g., in-game coins) or query purchase details via the Google Play Developer API.
|
|
151
162
|
|
|
152
163
|
### iOS-Only Purchase Types
|
|
153
164
|
|
|
154
|
-
- **`ProductPurchase`**:
|
|
155
|
-
-
|
|
165
|
+
- **`ProductPurchase`**:
|
|
166
|
+
- Extends the base purchase properties with iOS-specific fields:
|
|
167
|
+
- **`quantityIos`**: `number` - The quantity of the product purchased (e.g., how many units of an item were bought).
|
|
168
|
+
- **`expirationDateIos`**: `number?` - The expiration date of the purchase as a Unix timestamp (in milliseconds), if applicable (optional, may be null for non-expiring products).
|
|
169
|
+
- **`subscriptionGroupIdIos`**: `string?` - The identifier of the subscription group this product belongs to, used for managing related subscriptions in the App Store (optional, may be null for non-subscription products).
|
|
170
|
+
|
|
171
|
+
- **`SubscriptionPurchase`**:
|
|
172
|
+
- Extends the base purchase properties with iOS-specific subscription handling:
|
|
173
|
+
- Includes all fields from `ProductPurchase` where applicable, plus additional subscription-specific logic.
|
|
174
|
+
- May include fields like:
|
|
175
|
+
- **`expirationDateIos`**: `number?` - The date and time when the subscription expires, represented as a Unix timestamp (in milliseconds), unless auto-renewed.
|
|
176
|
+
- **`autoRenewingIos`**: `boolean` - Indicates whether the subscription is set to automatically renew (true) or not (false).
|
|
177
|
+
- Handles subscription-specific features such as renewals, grace periods, and App Store receipt validation.
|
|
156
178
|
|
|
157
179
|
## Implementation Notes
|
|
158
180
|
|
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -17,105 +17,84 @@ struct IapEvent {
|
|
|
17
17
|
|
|
18
18
|
@available(iOS 15.0, *)
|
|
19
19
|
func serializeTransaction(_ transaction: Transaction) -> [String: Any?] {
|
|
20
|
-
// Determine if this is a subscription by productType or expirationDate
|
|
21
20
|
let isSubscription =
|
|
22
21
|
transaction.productType.rawValue.lowercased().contains("renewable")
|
|
23
22
|
|| transaction.expirationDate != nil
|
|
24
23
|
|
|
25
|
-
// Parse transaction reason from jsonRepresentation if available
|
|
26
24
|
var transactionReasonIos: String? = nil
|
|
27
25
|
var webOrderLineItemId: Int? = nil
|
|
28
26
|
var jsonData: [String: Any]? = nil
|
|
27
|
+
var jwsReceipt: String = ""
|
|
28
|
+
|
|
29
|
+
let jsonRep = transaction.jsonRepresentation
|
|
30
|
+
jwsReceipt = String(data: jsonRep, encoding: .utf8) ?? ""
|
|
29
31
|
|
|
30
|
-
// JSON representation handling (JWS 데이터)
|
|
31
|
-
var jwsReceipt: String
|
|
32
32
|
do {
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
if let webOrderId = jsonObj["webOrderLineItemID"] as? NSNumber {
|
|
41
|
-
webOrderLineItemId = webOrderId.intValue
|
|
33
|
+
if let jsonObj = try JSONSerialization.jsonObject(with: jsonRep) as? [String: Any] {
|
|
34
|
+
jsonData = jsonObj
|
|
35
|
+
transactionReasonIos = jsonObj["transactionReason"] as? String
|
|
36
|
+
if let webOrderId = jsonObj["webOrderLineItemID"] as? NSNumber {
|
|
37
|
+
webOrderLineItemId = webOrderId.intValue
|
|
38
|
+
}
|
|
42
39
|
}
|
|
43
40
|
} catch {
|
|
44
41
|
print("Error parsing JSON representation: \(error)")
|
|
45
|
-
jwsReceipt = ""
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
// Create base purchase object that matches Purchase type in TypeScript
|
|
49
44
|
var purchaseMap: [String: Any?] = [
|
|
50
|
-
// Core purchase fields
|
|
51
45
|
"id": transaction.productID,
|
|
52
46
|
"ids": [transaction.productID],
|
|
53
47
|
"transactionId": String(transaction.id),
|
|
54
48
|
"transactionDate": transaction.purchaseDate.timeIntervalSince1970 * 1000,
|
|
55
|
-
"transactionReceipt": jwsReceipt,
|
|
49
|
+
"transactionReceipt": jwsReceipt,
|
|
56
50
|
|
|
57
|
-
// iOS specific fields - basic info
|
|
58
51
|
"quantityIos": transaction.purchasedQuantity,
|
|
59
52
|
"originalTransactionDateIos": transaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
60
53
|
"originalTransactionIdentifierIos": transaction.originalID,
|
|
61
54
|
"appAccountToken": transaction.appAccountToken?.uuidString,
|
|
62
55
|
|
|
63
|
-
// App and Product Identifiers
|
|
64
56
|
"appBundleIdIos": transaction.appBundleID,
|
|
65
57
|
"productTypeIos": transaction.productType.rawValue,
|
|
66
58
|
"subscriptionGroupIdIos": transaction.subscriptionGroupID,
|
|
67
59
|
|
|
68
|
-
// Transaction Identifiers
|
|
69
60
|
"webOrderLineItemIdIos": webOrderLineItemId,
|
|
70
61
|
|
|
71
|
-
// Purchase and Expiration Dates
|
|
72
62
|
"expirationDateIos": transaction.expirationDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
73
63
|
|
|
74
|
-
// Purchase Details
|
|
75
64
|
"isUpgradedIos": transaction.isUpgraded,
|
|
76
65
|
"ownershipTypeIos": transaction.ownershipType.rawValue,
|
|
77
66
|
|
|
78
|
-
// Revocation Status
|
|
79
67
|
"revocationDateIos": transaction.revocationDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
80
68
|
"revocationReasonIos": transaction.revocationReason?.rawValue,
|
|
69
|
+
"transactionReasonIos": transactionReasonIos,
|
|
81
70
|
]
|
|
82
71
|
|
|
83
|
-
// Environment (iOS 16.0+)
|
|
84
72
|
if #available(iOS 16.0, *) {
|
|
85
73
|
purchaseMap["environmentIos"] = transaction.environment.rawValue
|
|
86
74
|
}
|
|
87
75
|
|
|
88
|
-
// Storefront (iOS 17.0+)
|
|
89
76
|
if #available(iOS 17.0, *) {
|
|
90
77
|
purchaseMap["storefrontCountryCodeIos"] = transaction.storefront.countryCode
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Transaction Reason (iOS 17.0+)
|
|
94
|
-
if #available(iOS 17.0, *) {
|
|
95
78
|
purchaseMap["reasonIos"] = transaction.reason.rawValue
|
|
96
79
|
}
|
|
97
80
|
|
|
98
|
-
// reasonStringRepresentation은 Transaction에 없으므로 제거
|
|
99
|
-
purchaseMap["transactionReasonIos"] = transactionReasonIos
|
|
100
|
-
|
|
101
|
-
// Add offer information if available with proper availability check
|
|
102
81
|
if #available(iOS 17.2, *) {
|
|
103
82
|
if let offer = transaction.offer {
|
|
104
83
|
purchaseMap["offerIos"] = [
|
|
105
|
-
"id": offer.id
|
|
84
|
+
"id": offer.id,
|
|
106
85
|
"type": offer.type.rawValue,
|
|
107
86
|
"paymentMode": offer.paymentMode?.rawValue ?? "",
|
|
108
87
|
]
|
|
109
88
|
}
|
|
110
89
|
}
|
|
111
90
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
91
|
+
if #available(iOS 15.4, *), let jsonData = jsonData {
|
|
92
|
+
if let price = jsonData["price"] as? NSNumber {
|
|
93
|
+
purchaseMap["priceIos"] = price.doubleValue
|
|
94
|
+
}
|
|
95
|
+
if let currency = jsonData["currency"] as? String {
|
|
96
|
+
purchaseMap["currencyIos"] = currency
|
|
97
|
+
}
|
|
119
98
|
}
|
|
120
99
|
|
|
121
100
|
return purchaseMap
|
package/ios/Types.swift
CHANGED
|
@@ -14,7 +14,6 @@ public enum StoreError: Error {
|
|
|
14
14
|
|
|
15
15
|
enum IapErrors: String, CaseIterable {
|
|
16
16
|
case E_UNKNOWN = "E_UNKNOWN"
|
|
17
|
-
case E_NOT_INITIALIZED = "E_NOT_INITIALIZED"
|
|
18
17
|
case E_SERVICE_ERROR = "E_SERVICE_ERROR"
|
|
19
18
|
case E_USER_CANCELLED = "E_USER_CANCELLED"
|
|
20
19
|
case E_USER_ERROR = "E_USER_ERROR"
|
package/package.json
CHANGED
package/src/useIap.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getProducts,
|
|
8
8
|
getAvailablePurchases,
|
|
9
9
|
getPurchaseHistory,
|
|
10
|
+
finishTransaction as finishTransactionInternal,
|
|
10
11
|
getSubscriptions,
|
|
11
12
|
} from './';
|
|
12
13
|
import {useCallback, useEffect, useState, useRef} from 'react';
|
|
@@ -94,17 +95,14 @@ export function useIAP(): IAP_STATUS {
|
|
|
94
95
|
async ({
|
|
95
96
|
purchase,
|
|
96
97
|
isConsumable,
|
|
97
|
-
developerPayloadAndroid,
|
|
98
98
|
}: {
|
|
99
99
|
purchase: ProductPurchase;
|
|
100
100
|
isConsumable?: boolean;
|
|
101
|
-
developerPayloadAndroid?: string;
|
|
102
101
|
}): Promise<string | boolean | PurchaseResult | void> => {
|
|
103
102
|
try {
|
|
104
|
-
return await
|
|
103
|
+
return await finishTransactionInternal({
|
|
105
104
|
purchase,
|
|
106
105
|
isConsumable,
|
|
107
|
-
developerPayloadAndroid,
|
|
108
106
|
});
|
|
109
107
|
} catch (err) {
|
|
110
108
|
throw err;
|