expo-iap 2.2.7 → 2.2.8-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/ExpoIap.types.d.ts +1 -4
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js +0 -5
- package/build/ExpoIap.types.js.map +1 -1
- package/build/index.d.ts +13 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +66 -60
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +0 -5
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +0 -3
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +0 -5
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +0 -3
- package/build/modules/ios.js.map +1 -1
- package/build/useIap.d.ts +4 -2
- package/build/useIap.d.ts.map +1 -1
- package/build/useIap.js +10 -10
- package/build/useIap.js.map +1 -1
- package/iap.md +206 -220
- package/package.json +1 -1
- package/src/ExpoIap.types.ts +1 -5
- package/src/index.ts +126 -111
- package/src/modules/android.ts +0 -6
- package/src/modules/ios.ts +0 -6
- package/src/useIap.ts +18 -13
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,iBAAiB,IAAI,yBAAyB,EAC9C,gBAAgB,
|
|
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 requestPurchase as requestPurchaseInternal,\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 }: {\n purchase: Purchase;\n isConsumable?: boolean;\n }) => Promise<string | boolean | PurchaseResult | void>;\n getAvailablePurchases: (skus: string[]) => Promise<void>;\n getPurchaseHistories: (skus: string[]) => Promise<void>;\n getProducts: (skus: string[]) => Promise<void>;\n getSubscriptions: (skus: string[]) => Promise<void>;\n requestPurchase: typeof requestPurchaseInternal;\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 const subscriptionsRef = useRef<{\n purchaseUpdate?: Subscription;\n purchaseError?: Subscription;\n promotedProductsIos?: Subscription;\n }>({});\n\n const getProductsInternal = useCallback(\n async (skus: string[]): Promise<void> => {\n setProducts(await getProducts(skus));\n },\n [],\n );\n\n const getSubscriptionsInternal = useCallback(\n async (skus: string[]): Promise<void> => {\n setSubscriptions(await getSubscriptions(skus));\n },\n [],\n );\n\n const getAvailablePurchasesInternal = useCallback(async (): Promise<void> => {\n setAvailablePurchases(await getAvailablePurchases());\n }, []);\n\n const getPurchaseHistoriesInternal = 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 getAvailablePurchases: getAvailablePurchasesInternal,\n getPurchaseHistories: getPurchaseHistoriesInternal,\n getProducts: getProductsInternal,\n getSubscriptions: getSubscriptionsInternal,\n requestPurchase: requestPurchaseInternal,\n };\n}\n"]}
|
package/iap.md
CHANGED
|
@@ -1,84 +1,117 @@
|
|
|
1
1
|
# Expo IAP Documentation
|
|
2
2
|
|
|
3
|
-
> **Key Feature**: `expo-iap` works seamlessly with Expo's managed workflow—no
|
|
3
|
+
> **Key Feature**: `expo-iap` works seamlessly with Expo's managed workflow—no native code required! This is a major improvement over `react-native-iap`, making it ideal for small teams using Expo SDK.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
`expo-iap` is an Expo module for handling in-app purchases (IAP) on iOS (StoreKit 2) and Android (Google Play Billing). It supports consumables, non-consumables, and subscriptions.
|
|
7
|
+
`expo-iap` is an Expo module for handling in-app purchases (IAP) on iOS (StoreKit 2) and Android (Google Play Billing). It supports consumables, non-consumables, and subscriptions. Unlike [`react-native-iap`](https://github.com/hyochan/react-native-iap), which requires native setup, `expo-iap` integrates seamlessly into Expo's [managed workflow](https://docs.expo.dev/archive/managed-vs-bare)—no ejecting needed! However, you’ll need a [development client](https://docs.expo.dev/development/introduction/) instead of Expo Go for full functionality. Starting from version 2.2.8, most features of `react-native-iap` have been ported.
|
|
8
8
|
|
|
9
|
-
## Installation
|
|
9
|
+
## Installation
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
`expo-iap` is compatible with [Expo SDK](https://expo.dev) 51+ and supports both managed workflows and React Native CLI projects. Official documentation is in progress for SDK inclusion (see [Expo IAP Documentation](link-to-docs)).
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### Add the Package
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npm install expo-iap
|
|
17
|
+
```
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
npm install expo-iap
|
|
19
|
-
```
|
|
19
|
+
### Configure with Expo Config Plugin (Managed Workflow Only)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
For managed workflows, add `'expo-iap'` to the `plugins` array in your `app.json` or `app.config.js`:
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"expo": {
|
|
26
|
+
"plugins": ["expo-iap"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
This plugin automatically configures Android BILLING permissions and iOS setup, making it plug-and-play with a development client.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
## Managed Expo Projects
|
|
34
34
|
|
|
35
|
-
> **
|
|
35
|
+
> **No Native Code Required—Use a Development Client!**
|
|
36
|
+
> Unlike `react-native-iap`, `expo-iap` works in Expo’s managed workflow without native modifications. However, you’ll need a [development client](https://docs.expo.dev/development/introduction/) instead of Expo Go.
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
1. **Run Your App**
|
|
39
|
+
After adding the package and configuring the plugin, build a development client and test with:
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
```bash
|
|
42
|
+
expo run:android
|
|
43
|
+
expo run:ios
|
|
44
|
+
```
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
2. **Example**
|
|
47
|
+
Check out a working sample at [example/App.tsx](https://github.com/hyochan/expo-iap/blob/main/example/App.tsx). It demonstrates:
|
|
48
|
+
- Initializing the connection (`initConnection`)
|
|
49
|
+
- Fetching products/subscriptions (`getProducts`, `getSubscriptions`)
|
|
50
|
+
- Handling purchases (`requestPurchase`)
|
|
51
|
+
- Listening for updates/errors (`purchaseUpdatedListener`, `purchaseErrorListener`)
|
|
52
|
+
|
|
53
|
+
## React Native CLI Projects
|
|
54
|
+
|
|
55
|
+
For React Native CLI environments, you’ll need to manually configure native settings that the Expo config plugin handles automatically in managed workflows. Follow these steps:
|
|
56
|
+
|
|
57
|
+
### Prerequisites
|
|
58
|
+
|
|
59
|
+
Ensure the Expo package is installed:
|
|
42
60
|
|
|
43
61
|
```bash
|
|
44
|
-
|
|
62
|
+
npx install-expo-modules@latest
|
|
45
63
|
```
|
|
46
64
|
|
|
47
|
-
|
|
65
|
+
Then configure it as described in [Installing Expo Modules](https://docs.expo.dev/bare/installing-expo-modules/).
|
|
48
66
|
|
|
49
|
-
|
|
67
|
+
### iOS Configuration
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
Run `npx pod-install` to install native dependencies. Update your `ios/Podfile` to set the deployment target to `15.0` or higher (required for StoreKit 2):
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
platform :ios, '15.0'
|
|
55
73
|
```
|
|
56
74
|
|
|
57
|
-
###
|
|
75
|
+
### Android Configuration
|
|
58
76
|
|
|
59
|
-
|
|
77
|
+
Manually apply the following changes:
|
|
60
78
|
|
|
61
|
-
|
|
79
|
+
1. **Update `android/build.gradle`**
|
|
80
|
+
Add the `supportLibVersion` to the `ext` block:
|
|
62
81
|
|
|
63
|
-
|
|
82
|
+
```gradle
|
|
83
|
+
buildscript {
|
|
84
|
+
ext {
|
|
85
|
+
supportLibVersion = "28.0.0" // Add this line
|
|
86
|
+
// Other existing ext properties...
|
|
87
|
+
}
|
|
88
|
+
// ...
|
|
89
|
+
}
|
|
90
|
+
```
|
|
64
91
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
92
|
+
If there’s no `ext` block, append it at the end.
|
|
93
|
+
|
|
94
|
+
2. **Update `android/app/src/main/AndroidManifest.xml`**
|
|
95
|
+
Add the BILLING permission:
|
|
96
|
+
|
|
97
|
+
```xml
|
|
98
|
+
<manifest ...>
|
|
99
|
+
<uses-permission android:name="com.android.vending.BILLING" />
|
|
100
|
+
<!-- Other manifest content... -->
|
|
101
|
+
</manifest>
|
|
102
|
+
```
|
|
72
103
|
|
|
73
|
-
|
|
104
|
+
## Current State & Feedback
|
|
105
|
+
|
|
106
|
+
Updates are in progress to improve reliability and address remaining edge cases. For production apps, test thoroughly. Contributions (docs, code, or bug reports) are welcome—especially detailed error logs or use cases!
|
|
74
107
|
|
|
75
108
|
## IAP Types
|
|
76
109
|
|
|
77
|
-
`expo-iap` supports the following In-App Purchase types, aligned with platform-specific APIs (Google Play Billing for Android,
|
|
110
|
+
`expo-iap` supports the following In-App Purchase types, aligned with platform-specific APIs (Google Play Billing for Android, StoreKit 2 for iOS).
|
|
78
111
|
|
|
79
112
|
### Consumable
|
|
80
113
|
|
|
81
|
-
- **Description**: Items
|
|
114
|
+
- **Description**: Items consumed after purchase and repurchasable (e.g., in-game currency, boosts).
|
|
82
115
|
- **Behavior**: Requires acknowledgment to enable repurchasing.
|
|
83
116
|
- **Platforms**: Supported on Android and iOS.
|
|
84
117
|
|
|
@@ -123,7 +156,6 @@ This section outlines the properties of products supported by `expo-iap`.
|
|
|
123
156
|
### iOS-Only Product Types
|
|
124
157
|
|
|
125
158
|
- **`ProductIos`**
|
|
126
|
-
- `displayPrice: string`: Formatted price.
|
|
127
159
|
- `isFamilyShareable: boolean`: Family sharing support.
|
|
128
160
|
- `jsonRepresentation: string`: StoreKit 2 JSON data.
|
|
129
161
|
- `subscription: SubscriptionInfo`: Subscription details.
|
|
@@ -147,57 +179,38 @@ This section describes purchase properties in `expo-iap`.
|
|
|
147
179
|
### Android-Only Purchase Types
|
|
148
180
|
|
|
149
181
|
- **`ProductPurchase`**:
|
|
150
|
-
|
|
151
|
-
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
- **`signatureAndroid`**: `string` - The cryptographic signature from Google Play to verify the purchase's authenticity.
|
|
155
|
-
- **`purchaseStateAndroid`**: `number` - The state of the purchase (e.g., 0 = purchased, 1 = canceled, 2 = pending).
|
|
156
|
-
|
|
182
|
+
- `ids?: string[]`: List of product IDs (multi-item purchases).
|
|
183
|
+
- `dataAndroid?: string`: Raw purchase data from Google Play.
|
|
184
|
+
- `signatureAndroid?: string`: Cryptographic signature.
|
|
185
|
+
- `purchaseStateAndroid?: number`: Purchase state (e.g., 0 = purchased).
|
|
157
186
|
- **`SubscriptionPurchase`**:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
- **`autoRenewingAndroid`**: `boolean` - Indicates whether the subscription automatically renews (true) or not (false).
|
|
161
|
-
|
|
162
|
-
- **`purchaseTokenAndroid`**:
|
|
163
|
-
- **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.
|
|
187
|
+
- `autoRenewingAndroid?: boolean`: Indicates auto-renewal status.
|
|
188
|
+
- **`purchaseTokenAndroid?`**: Unique identifier for tracking/verifying purchases.
|
|
164
189
|
|
|
165
190
|
### iOS-Only Purchase Types
|
|
166
191
|
|
|
167
192
|
- **`ProductPurchase`**:
|
|
168
|
-
|
|
169
|
-
-
|
|
170
|
-
|
|
171
|
-
- **`expirationDateIos`**: `number?` - The expiration date of the purchase as a Unix timestamp (in milliseconds), if applicable (optional, may be null for non-expiring products).
|
|
172
|
-
- **`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).
|
|
173
|
-
|
|
193
|
+
- `quantityIos?: number`: Quantity purchased.
|
|
194
|
+
- `expirationDateIos?: number`: Expiration timestamp (optional).
|
|
195
|
+
- `subscriptionGroupIdIos?: string`: Subscription group ID (optional).
|
|
174
196
|
- **`SubscriptionPurchase`**:
|
|
175
|
-
- Extends
|
|
176
|
-
- Includes all fields from `ProductPurchase` where applicable, plus additional subscription-specific logic.
|
|
177
|
-
- May include fields like:
|
|
178
|
-
- **`expirationDateIos`**: `number?` - The date and time when the subscription expires, represented as a Unix timestamp (in milliseconds), unless auto-renewed.
|
|
179
|
-
- **`autoRenewingIos`**: `boolean` - Indicates whether the subscription is set to automatically renew (true) or not (false).
|
|
180
|
-
- Handles subscription-specific features such as renewals, grace periods, and App Store receipt validation.
|
|
197
|
+
- Extends `ProductPurchase` with subscription-specific fields like `expirationDateIos`.
|
|
181
198
|
|
|
182
199
|
## Implementation Notes
|
|
183
200
|
|
|
184
201
|
### Platform-Uniform Purchase Handling
|
|
185
202
|
|
|
186
|
-
Transactions
|
|
187
|
-
|
|
188
|
-
### Status
|
|
189
|
-
|
|
190
|
-
This module is under development—expect occasional bugs (e.g., Android acknowledgment issues). Test thoroughly and consider contributing fixes!
|
|
203
|
+
Transactions map to `Purchase` or `SubscriptionPurchase` with platform-specific fields (e.g., `expirationDateIos`, `purchaseStateAndroid`).
|
|
191
204
|
|
|
192
205
|
> **Sample Code**: See [example/App.tsx](https://github.com/hyochan/expo-iap/blob/main/example/App.tsx).
|
|
193
206
|
|
|
194
207
|
## Implementation
|
|
195
208
|
|
|
196
|
-
Below is a simple example of fetching products and making a purchase with `expo-iap` in a managed workflow:
|
|
209
|
+
Below is a simple example of fetching products and making a purchase with `expo-iap` in a managed workflow, updated to use the new `requestPurchase` signature:
|
|
197
210
|
|
|
198
211
|
```tsx
|
|
199
|
-
import {useEffect, useState} from 'react';
|
|
200
|
-
import {Button, Text, View} from 'react-native';
|
|
212
|
+
import { useEffect, useState } from 'react';
|
|
213
|
+
import { Button, Text, View } from 'react-native';
|
|
201
214
|
import {
|
|
202
215
|
initConnection,
|
|
203
216
|
endConnection,
|
|
@@ -216,16 +229,15 @@ export default function SimpleIAP() {
|
|
|
216
229
|
const setupIAP = async () => {
|
|
217
230
|
if (await initConnection()) {
|
|
218
231
|
setIsConnected(true);
|
|
219
|
-
const products = await getProducts(['my.consumable.item']); // Replace with your
|
|
232
|
+
const products = await getProducts(['my.consumable.item']); // Replace with your SKU
|
|
220
233
|
if (products.length > 0) setProduct(products[0]);
|
|
221
234
|
}
|
|
222
235
|
};
|
|
223
236
|
setupIAP();
|
|
224
237
|
|
|
225
|
-
// Handle purchase updates
|
|
226
238
|
const purchaseListener = purchaseUpdatedListener(async (purchase) => {
|
|
227
239
|
if (purchase) {
|
|
228
|
-
await finishTransaction({purchase, isConsumable: true});
|
|
240
|
+
await finishTransaction({ purchase, isConsumable: true });
|
|
229
241
|
alert('Purchase completed!');
|
|
230
242
|
}
|
|
231
243
|
});
|
|
@@ -239,11 +251,13 @@ export default function SimpleIAP() {
|
|
|
239
251
|
// Trigger a purchase
|
|
240
252
|
const buyItem = async () => {
|
|
241
253
|
if (!product) return;
|
|
242
|
-
await requestPurchase({
|
|
254
|
+
await requestPurchase({
|
|
255
|
+
request: { skus: [product.id] }, // Android expects 'skus'; iOS would use 'sku'
|
|
256
|
+
});
|
|
243
257
|
};
|
|
244
258
|
|
|
245
259
|
return (
|
|
246
|
-
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
|
|
260
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
247
261
|
<Text>{isConnected ? 'Connected' : 'Connecting...'}</Text>
|
|
248
262
|
{product ? (
|
|
249
263
|
<>
|
|
@@ -260,10 +274,10 @@ export default function SimpleIAP() {
|
|
|
260
274
|
|
|
261
275
|
## Using useIAP Hook
|
|
262
276
|
|
|
263
|
-
The `useIAP` hook simplifies managing in-app purchases
|
|
277
|
+
The `useIAP` hook simplifies managing in-app purchases. Below is an example updated to use the new `requestPurchase` signature:
|
|
264
278
|
|
|
265
279
|
```tsx
|
|
266
|
-
import {useEffect, useState} from 'react';
|
|
280
|
+
import { useEffect, useState } from 'react';
|
|
267
281
|
import {
|
|
268
282
|
SafeAreaView,
|
|
269
283
|
ScrollView,
|
|
@@ -275,34 +289,15 @@ import {
|
|
|
275
289
|
InteractionManager,
|
|
276
290
|
Alert,
|
|
277
291
|
} from 'react-native';
|
|
278
|
-
import {
|
|
279
|
-
import type {
|
|
280
|
-
PurchaseError,
|
|
281
|
-
ProductPurchase,
|
|
282
|
-
SubscriptionProduct,
|
|
283
|
-
} from 'expo-iap';
|
|
284
|
-
import {RequestSubscriptionAndroidProps} from './types/ExpoIapAndroid.types'; // Adjust path as needed
|
|
292
|
+
import { useIAP } from 'expo-iap';
|
|
293
|
+
import type { ProductPurchase, SubscriptionProduct } from 'expo-iap';
|
|
285
294
|
|
|
286
295
|
// Define SKUs
|
|
287
|
-
const productSkus = [
|
|
288
|
-
|
|
289
|
-
'cpk.points.5000',
|
|
290
|
-
'cpk.points.10000',
|
|
291
|
-
'cpk.points.30000',
|
|
292
|
-
];
|
|
293
|
-
|
|
294
|
-
const subscriptionSkus = [
|
|
295
|
-
'cpk.membership.monthly.bronze',
|
|
296
|
-
'cpk.membership.monthly.silver',
|
|
297
|
-
];
|
|
296
|
+
const productSkus = ['cpk.points.1000', 'cpk.points.5000', 'cpk.points.10000', 'cpk.points.30000'];
|
|
297
|
+
const subscriptionSkus = ['cpk.membership.monthly.bronze', 'cpk.membership.monthly.silver'];
|
|
298
298
|
|
|
299
299
|
// Define operations
|
|
300
|
-
const operations = [
|
|
301
|
-
'initConnection',
|
|
302
|
-
'getProducts',
|
|
303
|
-
'getSubscriptions',
|
|
304
|
-
'endConnection',
|
|
305
|
-
];
|
|
300
|
+
const operations = ['getProducts', 'getSubscriptions'] as const;
|
|
306
301
|
type Operation = (typeof operations)[number];
|
|
307
302
|
|
|
308
303
|
export default function IAPWithHook() {
|
|
@@ -316,38 +311,24 @@ export default function IAPWithHook() {
|
|
|
316
311
|
getSubscriptions,
|
|
317
312
|
finishTransaction,
|
|
318
313
|
requestPurchase,
|
|
319
|
-
requestSubscription,
|
|
320
314
|
} = useIAP();
|
|
321
315
|
|
|
322
|
-
const [
|
|
316
|
+
const [isReady, setIsReady] = useState(false);
|
|
323
317
|
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
break;
|
|
340
|
-
case 'getSubscriptions':
|
|
341
|
-
try {
|
|
342
|
-
await getSubscriptions(subscriptionSkus);
|
|
343
|
-
} catch (error) {
|
|
344
|
-
console.error('Error fetching subscriptions:', error);
|
|
345
|
-
}
|
|
346
|
-
break;
|
|
347
|
-
default:
|
|
348
|
-
console.log('Unknown operation');
|
|
349
|
-
}
|
|
350
|
-
};
|
|
318
|
+
// Fetch products and subscriptions only when connected
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (!connected) return;
|
|
321
|
+
|
|
322
|
+
const initializeIAP = async () => {
|
|
323
|
+
try {
|
|
324
|
+
await Promise.all([getProducts(productSkus), getSubscriptions(subscriptionSkus)]);
|
|
325
|
+
setIsReady(true);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error('Error initializing IAP:', error);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
initializeIAP();
|
|
331
|
+
}, [connected, getProducts, getSubscriptions]);
|
|
351
332
|
|
|
352
333
|
// Handle purchase updates and errors
|
|
353
334
|
useEffect(() => {
|
|
@@ -356,32 +337,59 @@ export default function IAPWithHook() {
|
|
|
356
337
|
try {
|
|
357
338
|
await finishTransaction({
|
|
358
339
|
purchase: currentPurchase,
|
|
359
|
-
isConsumable:
|
|
340
|
+
isConsumable: currentPurchase.productType === 'inapp',
|
|
360
341
|
});
|
|
361
|
-
Alert.alert('Purchase
|
|
342
|
+
Alert.alert('Purchase Successful', JSON.stringify(currentPurchase));
|
|
362
343
|
} catch (error) {
|
|
363
344
|
console.error('Error finishing transaction:', error);
|
|
345
|
+
Alert.alert('Transaction Error', String(error));
|
|
364
346
|
}
|
|
365
347
|
});
|
|
366
348
|
}
|
|
367
349
|
|
|
368
350
|
if (currentPurchaseError) {
|
|
369
351
|
InteractionManager.runAfterInteractions(() => {
|
|
370
|
-
Alert.alert('Purchase
|
|
352
|
+
Alert.alert('Purchase Error', JSON.stringify(currentPurchaseError));
|
|
371
353
|
});
|
|
372
354
|
}
|
|
373
355
|
}, [currentPurchase, currentPurchaseError, finishTransaction]);
|
|
374
356
|
|
|
357
|
+
// Handle operation buttons
|
|
358
|
+
const handleOperation = async (operation: Operation) => {
|
|
359
|
+
if (!connected) {
|
|
360
|
+
Alert.alert('Not Connected', 'Please wait for IAP to connect.');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
switch (operation) {
|
|
366
|
+
case 'getProducts':
|
|
367
|
+
await getProducts(productSkus);
|
|
368
|
+
break;
|
|
369
|
+
case 'getSubscriptions':
|
|
370
|
+
await getSubscriptions(subscriptionSkus);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.error(`Error in ${operation}:`, error);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
if (!connected) {
|
|
379
|
+
return (
|
|
380
|
+
<SafeAreaView style={styles.container}>
|
|
381
|
+
<Text style={styles.title}>Connecting to IAP...</Text>
|
|
382
|
+
</SafeAreaView>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
375
386
|
return (
|
|
376
387
|
<SafeAreaView style={styles.container}>
|
|
377
388
|
<Text style={styles.title}>Expo IAP with useIAP Hook</Text>
|
|
378
389
|
<View style={styles.buttons}>
|
|
379
390
|
<ScrollView contentContainerStyle={styles.buttonsWrapper} horizontal>
|
|
380
391
|
{operations.map((operation) => (
|
|
381
|
-
<Pressable
|
|
382
|
-
key={operation}
|
|
383
|
-
onPress={() => handleOperation(operation)}
|
|
384
|
-
>
|
|
392
|
+
<Pressable key={operation} onPress={() => handleOperation(operation)}>
|
|
385
393
|
<View style={styles.buttonView}>
|
|
386
394
|
<Text>{operation}</Text>
|
|
387
395
|
</View>
|
|
@@ -390,82 +398,60 @@ export default function IAPWithHook() {
|
|
|
390
398
|
</ScrollView>
|
|
391
399
|
</View>
|
|
392
400
|
<View style={styles.content}>
|
|
393
|
-
{!
|
|
394
|
-
<Text>
|
|
401
|
+
{!isReady ? (
|
|
402
|
+
<Text>Loading...</Text>
|
|
395
403
|
) : (
|
|
396
|
-
<View style={{gap: 12}}>
|
|
397
|
-
<Text style={{fontSize: 20}}>Products</Text>
|
|
398
|
-
{products.map((item) =>
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
],
|
|
448
|
-
}),
|
|
449
|
-
} as RequestSubscriptionAndroidProps)
|
|
450
|
-
}
|
|
451
|
-
/>
|
|
452
|
-
</View>
|
|
453
|
-
));
|
|
454
|
-
}
|
|
455
|
-
if (item.platform === 'ios') {
|
|
456
|
-
return (
|
|
457
|
-
<View key={item.id} style={{gap: 12}}>
|
|
458
|
-
<Text>
|
|
459
|
-
{item.displayName} - {item.displayPrice}
|
|
460
|
-
</Text>
|
|
461
|
-
<Button
|
|
462
|
-
title="Subscribe"
|
|
463
|
-
onPress={() => requestSubscription({sku: item.id})}
|
|
464
|
-
/>
|
|
465
|
-
</View>
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
})}
|
|
404
|
+
<View style={{ gap: 12 }}>
|
|
405
|
+
<Text style={{ fontSize: 20 }}>Products</Text>
|
|
406
|
+
{products.map((item) => (
|
|
407
|
+
<View key={item.id} style={{ gap: 12 }}>
|
|
408
|
+
<Text>
|
|
409
|
+
{item.title} -{' '}
|
|
410
|
+
{item.platform === 'android'
|
|
411
|
+
? item.oneTimePurchaseOfferDetails?.formattedPrice
|
|
412
|
+
: item.displayPrice}
|
|
413
|
+
</Text>
|
|
414
|
+
<Button
|
|
415
|
+
title="Buy"
|
|
416
|
+
onPress={() =>
|
|
417
|
+
requestPurchase({
|
|
418
|
+
request: item.platform === 'android' ? { skus: [item.id] } : { sku: item.id },
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
/>
|
|
422
|
+
</View>
|
|
423
|
+
))}
|
|
424
|
+
|
|
425
|
+
<Text style={{ fontSize: 20 }}>Subscriptions</Text>
|
|
426
|
+
{subscriptions.map((item) => (
|
|
427
|
+
<View key={item.id} style={{ gap: 12 }}>
|
|
428
|
+
<Text>
|
|
429
|
+
{item.title || item.displayName} -{' '}
|
|
430
|
+
{item.platform === 'android' && item.subscriptionOfferDetails
|
|
431
|
+
? item.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList[0].formattedPrice
|
|
432
|
+
: item.displayPrice}
|
|
433
|
+
</Text>
|
|
434
|
+
<Button
|
|
435
|
+
title="Subscribe"
|
|
436
|
+
onPress={() =>
|
|
437
|
+
requestPurchase({
|
|
438
|
+
request:
|
|
439
|
+
item.platform === 'android'
|
|
440
|
+
? {
|
|
441
|
+
skus: [item.id],
|
|
442
|
+
subscriptionOffers:
|
|
443
|
+
item.subscriptionOfferDetails?.map((offer) => ({
|
|
444
|
+
sku: item.id,
|
|
445
|
+
offerToken: offer.offerToken,
|
|
446
|
+
})) || [],
|
|
447
|
+
}
|
|
448
|
+
: { sku: item.id },
|
|
449
|
+
type: 'subs',
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
/>
|
|
453
|
+
</View>
|
|
454
|
+
))}
|
|
469
455
|
</View>
|
|
470
456
|
)}
|
|
471
457
|
</View>
|