expo-iap 2.4.5 → 2.5.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/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**
package/docs/README.md DELETED
@@ -1,30 +0,0 @@
1
- # Documentation
2
-
3
- This directory contains detailed documentation for expo-iap.
4
-
5
- ## Available Documents
6
-
7
- - **[API Documentation](./IAP.md)** - Complete API reference and usage examples
8
- - **[Error Code Management](./ERROR_CODES.md)** - Centralized error handling system documentation
9
-
10
- ## Quick Links
11
-
12
- - [Main README](../README.md) - Project overview and quick start
13
- - [Example App](../example/) - Sample implementation
14
- - [GitHub Issues](https://github.com/hyochan/expo-iap/issues) - Bug reports and feature requests
15
-
16
- ## Contributing
17
-
18
- If you find any documentation errors or have suggestions for improvements, please:
19
-
20
- 1. Check existing [issues](https://github.com/hyochan/expo-iap/issues)
21
- 2. Create a new issue with the `documentation` label
22
- 3. Submit a pull request with your improvements
23
-
24
- ## Support
25
-
26
- For questions and support:
27
-
28
- - 📚 Read the documentation in this folder
29
- - 🐛 Report bugs via [GitHub Issues](https://github.com/hyochan/expo-iap/issues)
30
- - 💬 Join discussions in [GitHub Discussions](https://github.com/hyochan/expo-iap/discussions)