expo-iap 2.5.0 → 2.5.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/.eslintignore +5 -0
- package/LICENSE +21 -0
- package/README.md +14 -8
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +14 -47
- package/android/src/main/java/expo/modules/iap/Types.kt +55 -0
- package/build/ExpoIap.types.d.ts +38 -13
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js +5 -0
- package/build/ExpoIap.types.js.map +1 -1
- package/build/useIap.d.ts +5 -3
- package/build/useIap.d.ts.map +1 -1
- package/build/useIap.js +24 -6
- package/build/useIap.js.map +1 -1
- package/build/utils/smartPurchase.d.ts +2 -0
- package/build/utils/smartPurchase.d.ts.map +1 -0
- package/build/utils/smartPurchase.js +2 -0
- package/build/utils/smartPurchase.js.map +1 -0
- package/ios/ExpoIapModule.swift +2 -27
- package/ios/Types.swift +34 -0
- package/package.json +7 -3
- package/src/ExpoIap.types.ts +77 -25
- package/src/useIap.ts +39 -24
- package/src/utils/smartPurchase.ts +0 -0
- package/docs/ERROR_CODES.md +0 -172
- package/docs/IAP.md +0 -500
package/docs/IAP.md
DELETED
|
@@ -1,500 +0,0 @@
|
|
|
1
|
-
# Expo IAP Documentation
|
|
2
|
-
|
|
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
|
-
|
|
5
|
-
## Overview
|
|
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. Unlike [`react-native-iap`](https://github.com/hyochan/react-native-iap), which requires native setup, `expo-iap` integrates seamlessly into Expo's managed workflow! 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
|
-
|
|
9
|
-
## Installation
|
|
10
|
-
|
|
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](https://github.com/hyochan/expo-iap/blob/main/docs/IAP.md#expo-iap-documentation)).
|
|
12
|
-
|
|
13
|
-
### Add the Package
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install expo-iap
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
### Configure with Expo Config Plugin (Managed Workflow Only)
|
|
20
|
-
|
|
21
|
-
For managed workflows, add `'expo-iap'` to the `plugins` array in your `app.json` or `app.config.js`:
|
|
22
|
-
|
|
23
|
-
```json
|
|
24
|
-
{
|
|
25
|
-
"expo": {
|
|
26
|
-
"plugins": ["expo-iap"]
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
This plugin automatically configures Android BILLING permissions and iOS setup, making it plug-and-play with a development client.
|
|
32
|
-
|
|
33
|
-
## Managed Expo Projects
|
|
34
|
-
|
|
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.
|
|
37
|
-
|
|
38
|
-
1. **Run Your App**
|
|
39
|
-
After adding the package and configuring the plugin, build a development client and test with:
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
expo run:android
|
|
43
|
-
expo run:ios
|
|
44
|
-
```
|
|
45
|
-
|
|
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:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
npx install-expo-modules@latest
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Then configure it as described in [Installing Expo Modules](https://docs.expo.dev/bare/installing-expo-modules/).
|
|
66
|
-
|
|
67
|
-
### iOS Configuration
|
|
68
|
-
|
|
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'
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Android Configuration
|
|
76
|
-
|
|
77
|
-
Manually apply the following changes:
|
|
78
|
-
|
|
79
|
-
1. **Update `android/build.gradle`**
|
|
80
|
-
Add the `supportLibVersion` to the `ext` block:
|
|
81
|
-
|
|
82
|
-
```gradle
|
|
83
|
-
buildscript {
|
|
84
|
-
ext {
|
|
85
|
-
supportLibVersion = "28.0.0" // Add this line
|
|
86
|
-
// Other existing ext properties...
|
|
87
|
-
}
|
|
88
|
-
// ...
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
If there’s no `ext` block, append it at the end.
|
|
93
|
-
|
|
94
|
-
## Current State & Feedback
|
|
95
|
-
|
|
96
|
-
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!
|
|
97
|
-
|
|
98
|
-
## IAP Types
|
|
99
|
-
|
|
100
|
-
`expo-iap` supports the following In-App Purchase types, aligned with platform-specific APIs (Google Play Billing for Android, StoreKit 2 for iOS).
|
|
101
|
-
|
|
102
|
-
### Consumable
|
|
103
|
-
|
|
104
|
-
- **Description**: Items consumed after purchase and repurchasable (e.g., in-game currency, boosts).
|
|
105
|
-
- **Behavior**: Requires acknowledgment to enable repurchasing.
|
|
106
|
-
- **Platforms**: Supported on Android and iOS.
|
|
107
|
-
|
|
108
|
-
### Non-Consumable
|
|
109
|
-
|
|
110
|
-
- **Description**: One-time purchases owned permanently (e.g., ad removal, premium features).
|
|
111
|
-
- **Behavior**: Supports restoration; cannot be repurchased.
|
|
112
|
-
- **Platforms**: Supported on Android and iOS.
|
|
113
|
-
|
|
114
|
-
### Subscription
|
|
115
|
-
|
|
116
|
-
- **Description**: Recurring purchases for ongoing access (e.g., monthly memberships).
|
|
117
|
-
- **Behavior**: Includes auto-renewing options and restoration.
|
|
118
|
-
- **Platforms**: Supported on Android and iOS.
|
|
119
|
-
|
|
120
|
-
## Product Type
|
|
121
|
-
|
|
122
|
-
This section outlines the properties of products supported by `expo-iap`.
|
|
123
|
-
|
|
124
|
-
### Common Product Types (`BaseProduct`)
|
|
125
|
-
|
|
126
|
-
| Property | Type | Description |
|
|
127
|
-
| -------------- | ------------- | -------------------------- |
|
|
128
|
-
| `id` | `string` | Unique product identifier |
|
|
129
|
-
| `title` | `string` | Product title |
|
|
130
|
-
| `description` | `string` | Product description |
|
|
131
|
-
| `type` | `ProductType` | `inapp` or `subs` |
|
|
132
|
-
| `displayName` | `string?` | UI display name (optional) |
|
|
133
|
-
| `displayPrice` | `string?` | Formatted price (optional) |
|
|
134
|
-
| `price` | `number?` | Price value (optional) |
|
|
135
|
-
| `currency` | `string?` | Currency code (optional) |
|
|
136
|
-
|
|
137
|
-
### Android-Only Product Types
|
|
138
|
-
|
|
139
|
-
- **`ProductAndroid`**
|
|
140
|
-
- `name: string`: Product name (replaces `displayName` on Android).
|
|
141
|
-
- `oneTimePurchaseOfferDetails?: OneTimePurchaseOfferDetails`: One-time purchase details.
|
|
142
|
-
- `subscriptionOfferDetails?: SubscriptionOfferDetail[]`: Subscription offer details.
|
|
143
|
-
- **`SubscriptionProductAndroid`**
|
|
144
|
-
- `subscriptionOfferDetails: SubscriptionOfferAndroid[]`: Subscription-specific offers.
|
|
145
|
-
|
|
146
|
-
### iOS-Only Product Types
|
|
147
|
-
|
|
148
|
-
- **`ProductIos`**
|
|
149
|
-
- `isFamilyShareable: boolean`: Family sharing support.
|
|
150
|
-
- `jsonRepresentation: string`: StoreKit 2 JSON data.
|
|
151
|
-
- `subscription: SubscriptionInfo`: Subscription details.
|
|
152
|
-
- **`SubscriptionProductIos`**
|
|
153
|
-
- `discounts?: Discount[]`: Discount details.
|
|
154
|
-
- `introductoryPrice?: string`: Introductory pricing info.
|
|
155
|
-
|
|
156
|
-
## Purchase Type
|
|
157
|
-
|
|
158
|
-
This section describes purchase properties in `expo-iap`.
|
|
159
|
-
|
|
160
|
-
### Common Purchase Types (`ProductPurchase`)
|
|
161
|
-
|
|
162
|
-
| Property | Type | Description |
|
|
163
|
-
| -------------------- | --------- | ------------------------- |
|
|
164
|
-
| `id` | `string` | Purchased product ID |
|
|
165
|
-
| `transactionId` | `string?` | Transaction ID (optional) |
|
|
166
|
-
| `transactionDate` | `number` | Unix timestamp |
|
|
167
|
-
| `transactionReceipt` | `string` | Receipt data |
|
|
168
|
-
|
|
169
|
-
### Android-Only Purchase Types
|
|
170
|
-
|
|
171
|
-
- **`ProductPurchase`**:
|
|
172
|
-
- `ids?: string[]`: List of product IDs (multi-item purchases).
|
|
173
|
-
- `dataAndroid?: string`: Raw purchase data from Google Play.
|
|
174
|
-
- `signatureAndroid?: string`: Cryptographic signature.
|
|
175
|
-
- `purchaseStateAndroid?: number`: Purchase state (e.g., 0 = purchased).
|
|
176
|
-
- **`SubscriptionPurchase`**:
|
|
177
|
-
- `autoRenewingAndroid?: boolean`: Indicates auto-renewal status.
|
|
178
|
-
- **`purchaseTokenAndroid?`**: Unique identifier for tracking/verifying purchases.
|
|
179
|
-
|
|
180
|
-
### iOS-Only Purchase Types
|
|
181
|
-
|
|
182
|
-
- **`ProductPurchase`**:
|
|
183
|
-
- `quantityIos?: number`: Quantity purchased.
|
|
184
|
-
- `expirationDateIos?: number`: Expiration timestamp (optional).
|
|
185
|
-
- `subscriptionGroupIdIos?: string`: Subscription group ID (optional).
|
|
186
|
-
- **`SubscriptionPurchase`**:
|
|
187
|
-
- Extends `ProductPurchase` with subscription-specific fields like `expirationDateIos`.
|
|
188
|
-
|
|
189
|
-
## Implementation Notes
|
|
190
|
-
|
|
191
|
-
### Platform-Uniform Purchase Handling
|
|
192
|
-
|
|
193
|
-
Transactions map to `Purchase` or `SubscriptionPurchase` with platform-specific fields (e.g., `expirationDateIos`, `purchaseStateAndroid`).
|
|
194
|
-
|
|
195
|
-
> **Sample Code**: See [example/App.tsx](https://github.com/hyochan/expo-iap/blob/main/example/App.tsx).
|
|
196
|
-
|
|
197
|
-
## Implementation
|
|
198
|
-
|
|
199
|
-
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:
|
|
200
|
-
|
|
201
|
-
```tsx
|
|
202
|
-
import {useEffect, useState} from 'react';
|
|
203
|
-
import {Button, Text, View} from 'react-native';
|
|
204
|
-
import {
|
|
205
|
-
initConnection,
|
|
206
|
-
endConnection,
|
|
207
|
-
getProducts,
|
|
208
|
-
requestPurchase,
|
|
209
|
-
purchaseUpdatedListener,
|
|
210
|
-
finishTransaction,
|
|
211
|
-
} from 'expo-iap';
|
|
212
|
-
|
|
213
|
-
export default function SimpleIAP() {
|
|
214
|
-
const [isConnected, setIsConnected] = useState(false);
|
|
215
|
-
const [product, setProduct] = useState(null);
|
|
216
|
-
|
|
217
|
-
// Initialize IAP and fetch products
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
const setupIAP = async () => {
|
|
220
|
-
if (await initConnection()) {
|
|
221
|
-
setIsConnected(true);
|
|
222
|
-
const products = await getProducts(['my.consumable.item']); // Replace with your SKU
|
|
223
|
-
if (products.length > 0) setProduct(products[0]);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
setupIAP();
|
|
227
|
-
|
|
228
|
-
const purchaseListener = purchaseUpdatedListener(async (purchase) => {
|
|
229
|
-
if (purchase) {
|
|
230
|
-
await finishTransaction({purchase, isConsumable: true});
|
|
231
|
-
alert('Purchase completed!');
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
return () => {
|
|
236
|
-
purchaseListener.remove();
|
|
237
|
-
endConnection();
|
|
238
|
-
};
|
|
239
|
-
}, []);
|
|
240
|
-
|
|
241
|
-
// Trigger a purchase
|
|
242
|
-
const buyItem = async () => {
|
|
243
|
-
if (!product) return;
|
|
244
|
-
await requestPurchase({
|
|
245
|
-
request: {skus: [product.id]}, // Android expects 'skus'; iOS would use 'sku'
|
|
246
|
-
});
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
return (
|
|
250
|
-
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
|
|
251
|
-
<Text>{isConnected ? 'Connected' : 'Connecting...'}</Text>
|
|
252
|
-
{product ? (
|
|
253
|
-
<>
|
|
254
|
-
<Text>{`${product.title} - ${product.displayPrice}`}</Text>
|
|
255
|
-
<Button title="Buy Now" onPress={buyItem} />
|
|
256
|
-
</>
|
|
257
|
-
) : (
|
|
258
|
-
<Text>Loading product...</Text>
|
|
259
|
-
)}
|
|
260
|
-
</View>
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
## Using `useIAP` Hook
|
|
266
|
-
|
|
267
|
-
The `useIAP` hook from `expo-iap` lets you manage in-app purchases in functional React components. This example reflects a **real-world production use case** with:
|
|
268
|
-
|
|
269
|
-
- Consumable & subscription support
|
|
270
|
-
- Purchase lifecycle management
|
|
271
|
-
- Platform-specific `requestPurchase` formats
|
|
272
|
-
- UX alerts and loading states
|
|
273
|
-
- Optional receipt validation before finishing the transaction
|
|
274
|
-
|
|
275
|
-
### 🧭 Flow Overview
|
|
276
|
-
|
|
277
|
-
| Step | Description |
|
|
278
|
-
| --- | --- |
|
|
279
|
-
| 1️⃣ | Wait for `connected === true` before fetching products and subscriptions |
|
|
280
|
-
| 2️⃣ | Render UI with products/subscriptions dynamically from store |
|
|
281
|
-
| 3️⃣ | Trigger purchases via `requestPurchase()` (with Android/iOS handling) |
|
|
282
|
-
| 4️⃣ | When `currentPurchase` updates, validate & finish the transaction |
|
|
283
|
-
| 5️⃣ | Handle `currentPurchaseError` for graceful UX |
|
|
284
|
-
|
|
285
|
-
### ✅ Realistic Example with `useIAP`
|
|
286
|
-
|
|
287
|
-
```tsx
|
|
288
|
-
import {useEffect, useState, useCallback} from 'react';
|
|
289
|
-
import {
|
|
290
|
-
View,
|
|
291
|
-
Text,
|
|
292
|
-
ScrollView,
|
|
293
|
-
Button,
|
|
294
|
-
Alert,
|
|
295
|
-
Platform,
|
|
296
|
-
InteractionManager,
|
|
297
|
-
} from 'react-native';
|
|
298
|
-
import {useIAP} from 'expo-iap';
|
|
299
|
-
import type {
|
|
300
|
-
ProductAndroid,
|
|
301
|
-
ProductPurchaseAndroid,
|
|
302
|
-
} from 'expo-iap/build/types/ExpoIapAndroid.types';
|
|
303
|
-
|
|
304
|
-
const productSkus = ['dev.hyo.luent.10bulbs', 'dev.hyo.luent.30bulbs'];
|
|
305
|
-
const subscriptionSkus = ['dev.hyo.luent.premium'];
|
|
306
|
-
|
|
307
|
-
export default function PurchaseScreen() {
|
|
308
|
-
const {
|
|
309
|
-
connected,
|
|
310
|
-
products,
|
|
311
|
-
subscriptions,
|
|
312
|
-
currentPurchase,
|
|
313
|
-
currentPurchaseError,
|
|
314
|
-
getProducts,
|
|
315
|
-
getSubscriptions,
|
|
316
|
-
requestPurchase,
|
|
317
|
-
finishTransaction,
|
|
318
|
-
validateReceipt,
|
|
319
|
-
} = useIAP();
|
|
320
|
-
|
|
321
|
-
const [isReady, setIsReady] = useState(false);
|
|
322
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
323
|
-
|
|
324
|
-
// 1️⃣ Initialize products & subscriptions
|
|
325
|
-
useEffect(() => {
|
|
326
|
-
if (!connected) return;
|
|
327
|
-
|
|
328
|
-
const loadStoreItems = async () => {
|
|
329
|
-
try {
|
|
330
|
-
await getProducts(productSkus);
|
|
331
|
-
await getSubscriptions(subscriptionSkus);
|
|
332
|
-
setIsReady(true);
|
|
333
|
-
} catch (e) {
|
|
334
|
-
console.error('IAP init error:', e);
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
loadStoreItems();
|
|
339
|
-
}, [connected]);
|
|
340
|
-
|
|
341
|
-
// 2️⃣ Purchase handler when currentPurchase updates
|
|
342
|
-
useEffect(() => {
|
|
343
|
-
if (!currentPurchase) return;
|
|
344
|
-
|
|
345
|
-
const handlePurchase = async () => {
|
|
346
|
-
try {
|
|
347
|
-
setIsLoading(true);
|
|
348
|
-
|
|
349
|
-
const productId = currentPurchase.id;
|
|
350
|
-
const isConsumable = productSkus.includes(productId);
|
|
351
|
-
|
|
352
|
-
// ✅ Optionally validate receipt before finishing
|
|
353
|
-
let isValid = true;
|
|
354
|
-
if (Platform.OS === 'ios') {
|
|
355
|
-
const result = await validateReceipt(productId);
|
|
356
|
-
isValid = result?.isValid ?? true;
|
|
357
|
-
} else if (Platform.OS === 'android') {
|
|
358
|
-
const token = (currentPurchase as ProductPurchaseAndroid)
|
|
359
|
-
.purchaseTokenAndroid;
|
|
360
|
-
const packageName = 'your.android.package.name';
|
|
361
|
-
|
|
362
|
-
const result = await validateReceipt(productId, {
|
|
363
|
-
productToken: token,
|
|
364
|
-
packageName,
|
|
365
|
-
isSub: subscriptionSkus.includes(productId),
|
|
366
|
-
});
|
|
367
|
-
isValid = result?.isValid ?? true;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (!isValid) {
|
|
371
|
-
Alert.alert('Invalid purchase', 'Receipt validation failed');
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// 🧾 Finish transaction (important!)
|
|
376
|
-
await finishTransaction({
|
|
377
|
-
purchase: currentPurchase,
|
|
378
|
-
isConsumable,
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
// ✅ Grant item or unlock feature
|
|
382
|
-
Alert.alert(
|
|
383
|
-
'Thank you!',
|
|
384
|
-
isConsumable
|
|
385
|
-
? 'Bulbs added to your account!'
|
|
386
|
-
: 'Premium subscription activated.',
|
|
387
|
-
);
|
|
388
|
-
} catch (err) {
|
|
389
|
-
console.error('Finish transaction error:', err);
|
|
390
|
-
} finally {
|
|
391
|
-
setIsLoading(false);
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
handlePurchase();
|
|
396
|
-
}, [currentPurchase]);
|
|
397
|
-
|
|
398
|
-
// 3️⃣ Error handling
|
|
399
|
-
useEffect(() => {
|
|
400
|
-
if (currentPurchaseError) {
|
|
401
|
-
InteractionManager.runAfterInteractions(() => {
|
|
402
|
-
Alert.alert('Purchase error', currentPurchaseError.message);
|
|
403
|
-
});
|
|
404
|
-
setIsLoading(false);
|
|
405
|
-
}
|
|
406
|
-
}, [currentPurchaseError]);
|
|
407
|
-
|
|
408
|
-
// 4️⃣ Purchase trigger
|
|
409
|
-
const handleBuy = useCallback(
|
|
410
|
-
async (productId: string, type?: 'subs') => {
|
|
411
|
-
try {
|
|
412
|
-
setIsLoading(true);
|
|
413
|
-
|
|
414
|
-
if (Platform.OS === 'ios') {
|
|
415
|
-
await requestPurchase({
|
|
416
|
-
request: {sku: productId},
|
|
417
|
-
type,
|
|
418
|
-
});
|
|
419
|
-
} else {
|
|
420
|
-
const request: any = {skus: [productId]};
|
|
421
|
-
|
|
422
|
-
if (type === 'subs') {
|
|
423
|
-
const sub = subscriptions.find(
|
|
424
|
-
(s) => s.id === productId,
|
|
425
|
-
) as ProductAndroid;
|
|
426
|
-
const offers =
|
|
427
|
-
sub?.subscriptionOfferDetails?.map((offer) => ({
|
|
428
|
-
sku: productId,
|
|
429
|
-
offerToken: offer.offerToken,
|
|
430
|
-
})) || [];
|
|
431
|
-
|
|
432
|
-
request.subscriptionOffers = offers;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
await requestPurchase({request, type});
|
|
436
|
-
}
|
|
437
|
-
} catch (err) {
|
|
438
|
-
console.error('Purchase request failed:', err);
|
|
439
|
-
Alert.alert(
|
|
440
|
-
'Error',
|
|
441
|
-
err instanceof Error ? err.message : 'Purchase failed',
|
|
442
|
-
);
|
|
443
|
-
setIsLoading(false);
|
|
444
|
-
}
|
|
445
|
-
},
|
|
446
|
-
[subscriptions],
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
if (!connected) return <Text>Connecting to store...</Text>;
|
|
450
|
-
if (!isReady) return <Text>Loading products...</Text>;
|
|
451
|
-
|
|
452
|
-
return (
|
|
453
|
-
<ScrollView contentContainerStyle={{padding: 20}}>
|
|
454
|
-
<Text style={{fontSize: 18, fontWeight: 'bold'}}>💡 Bulb Packs</Text>
|
|
455
|
-
{products.map((p) => (
|
|
456
|
-
<View key={p.id} style={{marginVertical: 10}}>
|
|
457
|
-
<Text>
|
|
458
|
-
{p.title} - {p.displayPrice}
|
|
459
|
-
</Text>
|
|
460
|
-
<Button
|
|
461
|
-
title={isLoading ? 'Processing...' : 'Buy'}
|
|
462
|
-
onPress={() => handleBuy(p.id)}
|
|
463
|
-
disabled={isLoading}
|
|
464
|
-
/>
|
|
465
|
-
</View>
|
|
466
|
-
))}
|
|
467
|
-
|
|
468
|
-
<Text style={{fontSize: 18, fontWeight: 'bold', marginTop: 30}}>
|
|
469
|
-
⭐ Subscription
|
|
470
|
-
</Text>
|
|
471
|
-
{subscriptions.map((s) => (
|
|
472
|
-
<View key={s.id} style={{marginVertical: 10}}>
|
|
473
|
-
<Text>
|
|
474
|
-
{s.title} - {s.displayPrice}
|
|
475
|
-
</Text>
|
|
476
|
-
<Button
|
|
477
|
-
title={isLoading ? 'Processing...' : 'Subscribe'}
|
|
478
|
-
onPress={() => handleBuy(s.id, 'subs')}
|
|
479
|
-
disabled={isLoading}
|
|
480
|
-
/>
|
|
481
|
-
</View>
|
|
482
|
-
))}
|
|
483
|
-
</ScrollView>
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
---
|
|
489
|
-
|
|
490
|
-
물론입니다. 아래는 보다 자연스럽고 요약된 느낌으로 다듬은 영어 버전입니다:
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
### 🔎 Key Benefits of This Approach
|
|
495
|
-
|
|
496
|
-
- ✅ Supports **both Android and iOS**, with platform-aware purchase handling
|
|
497
|
-
- ✅ Covers **both consumable items and subscriptions**
|
|
498
|
-
- ✅ Includes a **receipt validation flow** using `validateReceipt` (server-ready)
|
|
499
|
-
- ✅ Handles iOS cases where **auto-finishing transactions is disabled**
|
|
500
|
-
- ✅ Provides **user-friendly error and loading state management**
|