expo-iap 2.4.3 → 2.4.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/README.md +51 -1
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +31 -6
- package/build/ExpoIap.types.d.ts +116 -2
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js +142 -1
- package/build/ExpoIap.types.js.map +1 -1
- package/build/ExpoIapModule.d.ts +3 -2
- package/build/ExpoIapModule.d.ts.map +1 -1
- package/build/ExpoIapModule.js +4 -1
- package/build/ExpoIapModule.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/useIap.js.map +1 -1
- package/build/utils/errorMapping.d.ts +29 -0
- package/build/utils/errorMapping.d.ts.map +1 -0
- package/build/utils/errorMapping.js +79 -0
- package/build/utils/errorMapping.js.map +1 -0
- package/bun.lockb +0 -0
- package/docs/ERROR_CODES.md +164 -0
- package/{iap.md → docs/IAP.md} +191 -202
- package/docs/README.md +30 -0
- package/ios/ExpoIapModule.swift +69 -45
- package/ios/Types.swift +26 -18
- package/package.json +2 -2
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/ExpoIap.types.ts +173 -0
- package/src/ExpoIapModule.ts +6 -1
- package/src/index.ts +1 -0
- package/src/useIap.ts +1 -1
- package/src/utils/errorMapping.ts +88 -0
package/{iap.md → docs/IAP.md}
RENAMED
|
@@ -4,11 +4,11 @@
|
|
|
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. Unlike [`react-native-iap`](https://github.com/hyochan/react-native-iap), which requires native setup, `expo-iap` integrates seamlessly into Expo's
|
|
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
8
|
|
|
9
9
|
## Installation
|
|
10
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](
|
|
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
12
|
|
|
13
13
|
### Add the Package
|
|
14
14
|
|
|
@@ -91,16 +91,6 @@ Manually apply the following changes:
|
|
|
91
91
|
|
|
92
92
|
If there’s no `ext` block, append it at the end.
|
|
93
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
|
-
```
|
|
103
|
-
|
|
104
94
|
## Current State & Feedback
|
|
105
95
|
|
|
106
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!
|
|
@@ -272,43 +262,49 @@ export default function SimpleIAP() {
|
|
|
272
262
|
}
|
|
273
263
|
```
|
|
274
264
|
|
|
275
|
-
## Using useIAP Hook
|
|
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
276
|
|
|
277
|
-
|
|
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`
|
|
278
286
|
|
|
279
287
|
```tsx
|
|
280
|
-
import {useEffect, useState} from 'react';
|
|
288
|
+
import {useEffect, useState, useCallback} from 'react';
|
|
281
289
|
import {
|
|
282
|
-
SafeAreaView,
|
|
283
|
-
ScrollView,
|
|
284
|
-
StyleSheet,
|
|
285
|
-
Text,
|
|
286
290
|
View,
|
|
287
|
-
|
|
291
|
+
Text,
|
|
292
|
+
ScrollView,
|
|
288
293
|
Button,
|
|
289
|
-
InteractionManager,
|
|
290
294
|
Alert,
|
|
295
|
+
Platform,
|
|
296
|
+
InteractionManager,
|
|
291
297
|
} from 'react-native';
|
|
292
298
|
import {useIAP} from 'expo-iap';
|
|
293
|
-
import type {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const subscriptionSkus = [
|
|
303
|
-
'cpk.membership.monthly.bronze',
|
|
304
|
-
'cpk.membership.monthly.silver',
|
|
305
|
-
];
|
|
306
|
-
|
|
307
|
-
// Define operations
|
|
308
|
-
const operations = ['getProducts', 'getSubscriptions'] as const;
|
|
309
|
-
type Operation = (typeof operations)[number];
|
|
310
|
-
|
|
311
|
-
export default function IAPWithHook() {
|
|
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() {
|
|
312
308
|
const {
|
|
313
309
|
connected,
|
|
314
310
|
products,
|
|
@@ -317,195 +313,188 @@ export default function IAPWithHook() {
|
|
|
317
313
|
currentPurchaseError,
|
|
318
314
|
getProducts,
|
|
319
315
|
getSubscriptions,
|
|
320
|
-
finishTransaction,
|
|
321
316
|
requestPurchase,
|
|
317
|
+
finishTransaction,
|
|
318
|
+
validateReceipt,
|
|
322
319
|
} = useIAP();
|
|
323
320
|
|
|
324
321
|
const [isReady, setIsReady] = useState(false);
|
|
322
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
325
323
|
|
|
326
|
-
//
|
|
324
|
+
// 1️⃣ Initialize products & subscriptions
|
|
327
325
|
useEffect(() => {
|
|
328
326
|
if (!connected) return;
|
|
329
327
|
|
|
330
|
-
const
|
|
328
|
+
const loadStoreItems = async () => {
|
|
331
329
|
try {
|
|
332
|
-
await
|
|
333
|
-
|
|
334
|
-
getSubscriptions(subscriptionSkus),
|
|
335
|
-
]);
|
|
330
|
+
await getProducts(productSkus);
|
|
331
|
+
await getSubscriptions(subscriptionSkus);
|
|
336
332
|
setIsReady(true);
|
|
337
|
-
} catch (
|
|
338
|
-
console.error('
|
|
333
|
+
} catch (e) {
|
|
334
|
+
console.error('IAP init error:', e);
|
|
339
335
|
}
|
|
340
336
|
};
|
|
341
|
-
initializeIAP();
|
|
342
|
-
}, [connected, getProducts, getSubscriptions]);
|
|
343
337
|
|
|
344
|
-
|
|
338
|
+
loadStoreItems();
|
|
339
|
+
}, [connected]);
|
|
340
|
+
|
|
341
|
+
// 2️⃣ Purchase handler when currentPurchase updates
|
|
345
342
|
useEffect(() => {
|
|
346
|
-
if (currentPurchase)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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),
|
|
352
366
|
});
|
|
353
|
-
|
|
354
|
-
} catch (error) {
|
|
355
|
-
console.error('Error finishing transaction:', error);
|
|
356
|
-
Alert.alert('Transaction Error', String(error));
|
|
367
|
+
isValid = result?.isValid ?? true;
|
|
357
368
|
}
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
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(() => {
|
|
361
400
|
if (currentPurchaseError) {
|
|
362
401
|
InteractionManager.runAfterInteractions(() => {
|
|
363
|
-
Alert.alert('Purchase
|
|
402
|
+
Alert.alert('Purchase error', currentPurchaseError.message);
|
|
364
403
|
});
|
|
404
|
+
setIsLoading(false);
|
|
365
405
|
}
|
|
366
|
-
}, [
|
|
406
|
+
}, [currentPurchaseError]);
|
|
367
407
|
|
|
368
|
-
//
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
408
|
+
// 4️⃣ Purchase trigger
|
|
409
|
+
const handleBuy = useCallback(
|
|
410
|
+
async (productId: string, type?: 'subs') => {
|
|
411
|
+
try {
|
|
412
|
+
setIsLoading(true);
|
|
374
413
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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);
|
|
383
444
|
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
};
|
|
445
|
+
},
|
|
446
|
+
[subscriptions],
|
|
447
|
+
);
|
|
388
448
|
|
|
389
|
-
if (!connected)
|
|
390
|
-
|
|
391
|
-
<SafeAreaView style={styles.container}>
|
|
392
|
-
<Text style={styles.title}>Connecting to IAP...</Text>
|
|
393
|
-
</SafeAreaView>
|
|
394
|
-
);
|
|
395
|
-
}
|
|
449
|
+
if (!connected) return <Text>Connecting to store...</Text>;
|
|
450
|
+
if (!isReady) return <Text>Loading products...</Text>;
|
|
396
451
|
|
|
397
452
|
return (
|
|
398
|
-
<
|
|
399
|
-
<Text style={
|
|
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
|
-
title="Buy"
|
|
430
|
-
onPress={() =>
|
|
431
|
-
requestPurchase({
|
|
432
|
-
request:
|
|
433
|
-
item.platform === 'android'
|
|
434
|
-
? {skus: [item.id]}
|
|
435
|
-
: {sku: item.id},
|
|
436
|
-
})
|
|
437
|
-
}
|
|
438
|
-
/>
|
|
439
|
-
</View>
|
|
440
|
-
))}
|
|
441
|
-
|
|
442
|
-
<Text style={{fontSize: 20}}>Subscriptions</Text>
|
|
443
|
-
{subscriptions.map((item) => (
|
|
444
|
-
<View key={item.id} style={{gap: 12}}>
|
|
445
|
-
<Text>
|
|
446
|
-
{item.title || item.displayName} -{' '}
|
|
447
|
-
{item.platform === 'android' && item.subscriptionOfferDetails
|
|
448
|
-
? item.subscriptionOfferDetails[0]?.pricingPhases
|
|
449
|
-
.pricingPhaseList[0].formattedPrice
|
|
450
|
-
: item.displayPrice}
|
|
451
|
-
</Text>
|
|
452
|
-
<Button
|
|
453
|
-
title="Subscribe"
|
|
454
|
-
onPress={() =>
|
|
455
|
-
requestPurchase({
|
|
456
|
-
request:
|
|
457
|
-
item.platform === 'android'
|
|
458
|
-
? {
|
|
459
|
-
skus: [item.id],
|
|
460
|
-
subscriptionOffers:
|
|
461
|
-
item.subscriptionOfferDetails?.map((offer) => ({
|
|
462
|
-
sku: item.id,
|
|
463
|
-
offerToken: offer.offerToken,
|
|
464
|
-
})) || [],
|
|
465
|
-
}
|
|
466
|
-
: {sku: item.id},
|
|
467
|
-
type: 'subs',
|
|
468
|
-
})
|
|
469
|
-
}
|
|
470
|
-
/>
|
|
471
|
-
</View>
|
|
472
|
-
))}
|
|
473
|
-
</View>
|
|
474
|
-
)}
|
|
475
|
-
</View>
|
|
476
|
-
</SafeAreaView>
|
|
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>
|
|
477
484
|
);
|
|
478
485
|
}
|
|
479
|
-
|
|
480
|
-
const styles = StyleSheet.create({
|
|
481
|
-
container: {
|
|
482
|
-
flex: 1,
|
|
483
|
-
backgroundColor: '#fff',
|
|
484
|
-
alignItems: 'center',
|
|
485
|
-
},
|
|
486
|
-
title: {
|
|
487
|
-
marginTop: 24,
|
|
488
|
-
fontSize: 20,
|
|
489
|
-
fontWeight: 'bold',
|
|
490
|
-
},
|
|
491
|
-
buttons: {
|
|
492
|
-
height: 90,
|
|
493
|
-
},
|
|
494
|
-
buttonsWrapper: {
|
|
495
|
-
padding: 24,
|
|
496
|
-
gap: 8,
|
|
497
|
-
},
|
|
498
|
-
buttonView: {
|
|
499
|
-
borderRadius: 8,
|
|
500
|
-
borderWidth: 1,
|
|
501
|
-
borderColor: '#000',
|
|
502
|
-
padding: 8,
|
|
503
|
-
},
|
|
504
|
-
content: {
|
|
505
|
-
flex: 1,
|
|
506
|
-
alignSelf: 'stretch',
|
|
507
|
-
padding: 24,
|
|
508
|
-
gap: 12,
|
|
509
|
-
},
|
|
510
|
-
});
|
|
511
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
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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)
|