@ridwan-retainer/paywall 0.1.0 → 0.1.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.
Files changed (2) hide show
  1. package/README.md +1001 -31
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,64 +1,1034 @@
1
- # @factory/paywall
1
+ # @ridwan-retainer/paywall
2
2
 
3
- Revenue-first monetization layer using RevenueCat for in-app purchases and subscriptions in React Native applications.
3
+ A complete, production-ready monetization solution for React Native apps using RevenueCat. Implements in-app purchases, subscriptions, paywalls, and entitlement management with a developer-friendly API.
4
4
 
5
- ## Features
5
+ [![npm version](https://badge.fury.io/js/@ridwan-retainer%2Fpaywall.svg)](https://www.npmjs.com/package/@ridwan-retainer/paywall)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
7
 
7
- - RevenueCat integration for cross-platform subscription management
8
- - Pre-built paywall UI components
9
- - Subscription status tracking and management
10
- - Purchase flow handling with proper error management
11
- - Offering and package management
12
- - TypeScript support with full type definitions
8
+ ## 📋 Table of Contents
13
9
 
14
- ## Installation
10
+ - [Features](#features)
11
+ - [Installation](#installation)
12
+ - [Quick Start](#quick-start)
13
+ - [Core Concepts](#core-concepts)
14
+ - [API Reference](#api-reference)
15
+ - [Initialization](#initialization)
16
+ - [Hooks](#hooks)
17
+ - [Components](#components)
18
+ - [Managers](#managers)
19
+ - [Complete Examples](#complete-examples)
20
+ - [Testing](#testing)
21
+ - [Best Practices](#best-practices)
22
+ - [Troubleshooting](#troubleshooting)
23
+
24
+ ## ✨ Features
25
+
26
+ - **💰 RevenueCat Integration**: Full SDK integration with cross-platform support
27
+ - **🎨 Pre-built Components**: Ready-to-use paywall, purchase buttons, and subscription gates
28
+ - **🔐 Entitlement Management**: Automatic entitlement checking and caching
29
+ - **♻️ Restore Purchases**: Built-in restore functionality with error handling
30
+ - **📦 Offering Management**: Easy access to products and packages
31
+ - **🎯 Feature Gates**: Control feature access based on subscriptions
32
+ - **⚡ Performance**: Optimized caching and minimal re-renders
33
+ - **🪝 React Hooks**: Modern hooks-based API
34
+ - **📝 TypeScript**: Full type safety
35
+ - **✅ Well-tested**: Comprehensive test coverage
36
+
37
+ ## 📦 Installation
15
38
 
16
39
  ```bash
17
- npm install @factory/paywall
40
+ npm install @ridwan-retainer/paywall
18
41
  ```
19
42
 
20
43
  ### Peer Dependencies
21
44
 
22
- This package requires the following peer dependencies:
45
+ ```bash
46
+ npm install react-native-purchases \
47
+ react-native-purchases-ui \
48
+ @react-native-async-storage/async-storage \
49
+ react \
50
+ react-native
51
+ ```
52
+
53
+ ### Platform Setup
23
54
 
55
+ #### iOS Setup
56
+
57
+ 1. **Install CocoaPods dependencies:**
24
58
  ```bash
25
- npm install react-native-purchases react-native-purchases-ui @react-native-async-storage/async-storage
59
+ cd ios && pod install
60
+ ```
61
+
62
+ 2. **Configure App Store Connect:**
63
+ - Create your app in App Store Connect
64
+ - Set up in-app products and subscriptions
65
+ - Submit for review
66
+
67
+ 3. **Add StoreKit configuration** (for testing in Xcode):
68
+ - File → New → File → StoreKit Configuration File
69
+ - Add your products for local testing
70
+
71
+ #### Android Setup
72
+
73
+ 1. **Configure Google Play Console:**
74
+ - Create your app in Google Play Console
75
+ - Set up in-app products and subscriptions
76
+ - Add test users for testing
77
+
78
+ 2. **Add billing permission** in `android/app/src/main/AndroidManifest.xml`:
79
+ ```xml
80
+ <uses-permission android:name="com.android.vending.BILLING" />
26
81
  ```
27
82
 
28
- ## Setup
83
+ ### RevenueCat Setup
84
+
85
+ 1. **Create RevenueCat account:** https://app.revenuecat.com/signup
86
+ 2. **Create a project** and configure your app
87
+ 3. **Link App Store/Google Play:**
88
+ - iOS: Add App Store Connect API Key
89
+ - Android: Add Google Play Service Account JSON
90
+ 4. **Set up products:**
91
+ - Create offerings
92
+ - Add products to offerings
93
+ - Configure entitlements
94
+ 5. **Get API keys:**
95
+ - Settings → API Keys
96
+ - Copy iOS and Android keys
97
+
98
+ ## 🚀 Quick Start
29
99
 
30
- 1. Create a RevenueCat account at [revenuecat.com](https://www.revenuecat.com)
31
- 2. Configure your app in the RevenueCat dashboard
32
- 3. Set up your products in App Store Connect and Google Play Console
33
- 4. Link your products to RevenueCat
100
+ ### 1. Configure Environment Variables
101
+
102
+ Create a `.env` file:
103
+
104
+ ```env
105
+ EXPO_PUBLIC_REVENUECAT_IOS=your_ios_api_key
106
+ EXPO_PUBLIC_REVENUECAT_ANDROID=your_android_api_key
107
+ ```
34
108
 
35
- ## Usage
109
+ ### 2. Initialize RevenueCat
36
110
 
37
111
  ```typescript
38
- import { PaywallProvider, usePaywall, PaywallView } from '@factory/paywall';
112
+ import { useRevenueCatInitialization } from '@ridwan-retainer/paywall';
113
+ import { useEffect } from 'react';
39
114
 
40
- // Wrap your app with PaywallProvider
41
115
  function App() {
116
+ const { isInitialized, error } = useRevenueCatInitialization({
117
+ appUserID: 'user_123', // Optional: your user ID
118
+ });
119
+
120
+ if (error) {
121
+ console.error('RevenueCat initialization failed:', error);
122
+ }
123
+
124
+ return isInitialized ? <YourApp /> : <LoadingScreen />;
125
+ }
126
+ ```
127
+
128
+ ### 3. Display a Paywall
129
+
130
+ ```typescript
131
+ import { Paywall } from '@ridwan-retainer/paywall';
132
+
133
+ function PaywallScreen() {
134
+ return (
135
+ <Paywall
136
+ onPurchaseCompleted={({ customerInfo }) => {
137
+ console.log('Purchase successful!', customerInfo);
138
+ // Navigate away or show success message
139
+ }}
140
+ onDismiss={() => {
141
+ console.log('Paywall dismissed');
142
+ }}
143
+ onPurchaseError={({ error }) => {
144
+ console.error('Purchase failed:', error);
145
+ }}
146
+ />
147
+ );
148
+ }
149
+ ```
150
+
151
+ ### 4. Gate Features
152
+
153
+ ```typescript
154
+ import { SubscriptionGate } from '@ridwan-retainer/paywall';
155
+
156
+ function PremiumFeature() {
157
+ return (
158
+ <SubscriptionGate>
159
+ <Text>This is premium content!</Text>
160
+ <PremiumFeatureContent />
161
+ </SubscriptionGate>
162
+ );
163
+ }
164
+ ```
165
+
166
+ ## 🧠 Core Concepts
167
+
168
+ ### Offerings and Packages
169
+
170
+ **Offerings** are collections of products (packages) you want to sell. Create them in RevenueCat dashboard.
171
+
172
+ **Packages** are individual subscription or purchase options (e.g., Monthly, Annual).
173
+
174
+ ```typescript
175
+ import { useCurrentOffering } from '@ridwan-retainer/paywall';
176
+
177
+ function ProductList() {
178
+ const { offering, isLoading } = useCurrentOffering();
179
+
180
+ if (isLoading) return <Loading />;
181
+
42
182
  return (
43
- <PaywallProvider revenueCatApiKey="your-api-key">
44
- <YourApp />
45
- </PaywallProvider>
183
+ <View>
184
+ {offering?.availablePackages.map((pkg) => (
185
+ <Text key={pkg.identifier}>
186
+ {pkg.product.title} - {pkg.product.priceString}
187
+ </Text>
188
+ ))}
189
+ </View>
190
+ );
191
+ }
192
+ ```
193
+
194
+ ### Entitlements
195
+
196
+ **Entitlements** represent access to features (configured in RevenueCat dashboard).
197
+
198
+ ```typescript
199
+ import { useEntitlement } from '@ridwan-retainer/paywall';
200
+
201
+ function PremiumButton() {
202
+ const { hasEntitlement, isLoading } = useEntitlement('premium');
203
+
204
+ return (
205
+ <Button disabled={!hasEntitlement}>
206
+ {hasEntitlement ? 'Premium Feature' : 'Upgrade to Access'}
207
+ </Button>
46
208
  );
47
209
  }
210
+ ```
211
+
212
+ ### Customer Info
213
+
214
+ **Customer Info** contains all subscription and purchase data for a user.
215
+
216
+ ```typescript
217
+ import { useCustomerInfo } from '@ridwan-retainer/paywall';
218
+
219
+ function SubscriptionStatus() {
220
+ const { customerInfo, isLoading } = useCustomerInfo();
221
+
222
+ const activeSub = customerInfo?.activeSubscriptions[0];
223
+
224
+ return (
225
+ <Text>
226
+ Status: {activeSub ? 'Subscribed' : 'Free'}
227
+ </Text>
228
+ );
229
+ }
230
+ ```
231
+
232
+ ## 📚 API Reference
233
+
234
+ ### Initialization
235
+
236
+ #### `useRevenueCatInitialization(config?: RevenueCatConfig)`
237
+
238
+ Initialize RevenueCat SDK. Call once at app startup.
239
+
240
+ ```typescript
241
+ import { useRevenueCatInitialization } from '@ridwan-retainer/paywall';
242
+
243
+ function App() {
244
+ const { isInitialized, error } = useRevenueCatInitialization({
245
+ appUserID: 'user_123', // Optional: Custom user ID
246
+ observerMode: false, // Optional: Observer mode (default: false)
247
+ useAmazon: false, // Optional: Use Amazon store (default: false)
248
+ });
249
+
250
+ return isInitialized ? <Main /> : <Splash />;
251
+ }
252
+ ```
253
+
254
+ **Returns:**
255
+ ```typescript
256
+ {
257
+ isInitialized: boolean;
258
+ error: Error | null;
259
+ }
260
+ ```
261
+
262
+ #### `initializeRevenueCat(apiKey: string, config?: RevenueCatConfig): Promise<void>`
263
+
264
+ Programmatic initialization (alternative to hook).
265
+
266
+ ```typescript
267
+ import { initializeRevenueCat } from '@ridwan-retainer/paywall';
268
+
269
+ await initializeRevenueCat('rcb_abc123', {
270
+ appUserID: 'user_123',
271
+ });
272
+ ```
273
+
274
+ ### Hooks
275
+
276
+ #### `useOfferings()`
48
277
 
49
- // Use the paywall hook in your components
50
- function YourComponent() {
51
- const { customerInfo, isSubscribed, offerings } = usePaywall();
278
+ Get all available offerings.
279
+
280
+ ```typescript
281
+ import { useOfferings } from '@ridwan-retainer/paywall';
282
+
283
+ function OfferingsList() {
284
+ const { offerings, currentOffering, isLoading, error } = useOfferings();
285
+
286
+ if (isLoading) return <Loading />;
287
+ if (error) return <Error message={error.message} />;
288
+
289
+ return (
290
+ <View>
291
+ {offerings?.all && Object.values(offerings.all).map((offering) => (
292
+ <OfferingCard key={offering.identifier} offering={offering} />
293
+ ))}
294
+ </View>
295
+ );
296
+ }
297
+ ```
298
+
299
+ **Returns:**
300
+ ```typescript
301
+ {
302
+ offerings: PurchasesOfferings | null;
303
+ currentOffering: PurchasesOffering | null;
304
+ isLoading: boolean;
305
+ error: Error | null;
306
+ refresh: () => Promise<void>;
307
+ }
308
+ ```
309
+
310
+ #### `useCurrentOffering()`
311
+
312
+ Get the current (default) offering.
313
+
314
+ ```typescript
315
+ import { useCurrentOffering } from '@ridwan-retainer/paywall';
316
+
317
+ function SubscriptionPlans() {
318
+ const { offering, isLoading } = useCurrentOffering();
319
+
320
+ return (
321
+ <View>
322
+ {offering?.availablePackages.map((pkg) => (
323
+ <PlanCard key={pkg.identifier} package={pkg} />
324
+ ))}
325
+ </View>
326
+ );
327
+ }
328
+ ```
329
+
330
+ **Returns:**
331
+ ```typescript
332
+ {
333
+ offering: PurchasesOffering | null;
334
+ isLoading: boolean;
335
+ error: Error | null;
336
+ refresh: () => Promise<void>;
337
+ }
338
+ ```
339
+
340
+ #### `useCustomerInfo()`
341
+
342
+ Get customer subscription information.
343
+
344
+ ```typescript
345
+ import { useCustomerInfo } from '@ridwan-retainer/paywall';
346
+
347
+ function AccountScreen() {
348
+ const { customerInfo, isLoading, refresh } = useCustomerInfo();
349
+
350
+ const isPremium = customerInfo?.entitlements.active['premium']?.isActive;
351
+ const expirationDate = customerInfo?.entitlements.active['premium']?.expirationDate;
352
+
353
+ return (
354
+ <View>
355
+ <Text>Status: {isPremium ? 'Premium' : 'Free'}</Text>
356
+ {expirationDate && (
357
+ <Text>Expires: {new Date(expirationDate).toLocaleDateString()}</Text>
358
+ )}
359
+ <Button onPress={refresh}>Refresh</Button>
360
+ </View>
361
+ );
362
+ }
363
+ ```
364
+
365
+ **Returns:**
366
+ ```typescript
367
+ {
368
+ customerInfo: CustomerInfo | null;
369
+ isLoading: boolean;
370
+ error: Error | null;
371
+ refresh: () => Promise<void>;
372
+ }
373
+ ```
374
+
375
+ #### `useEntitlement(entitlementId: string)`
376
+
377
+ Check if user has a specific entitlement.
378
+
379
+ ```typescript
380
+ import { useEntitlement } from '@ridwan-retainer/paywall';
381
+
382
+ function PremiumFeatureButton() {
383
+ const { hasEntitlement, isLoading, entitlementInfo } = useEntitlement('premium');
384
+
385
+ if (isLoading) return <Loading />;
386
+
387
+ return (
388
+ <Button disabled={!hasEntitlement}>
389
+ {hasEntitlement ? 'Access Premium' : 'Upgrade Required'}
390
+ </Button>
391
+ );
392
+ }
393
+ ```
394
+
395
+ **Returns:**
396
+ ```typescript
397
+ {
398
+ hasEntitlement: boolean;
399
+ entitlementInfo: PurchasesEntitlementInfo | null;
400
+ isLoading: boolean;
401
+ error: Error | null;
402
+ }
403
+ ```
404
+
405
+ #### `usePurchase()`
406
+
407
+ Handle purchase flow.
408
+
409
+ ```typescript
410
+ import { usePurchase } from '@ridwan-retainer/paywall';
411
+
412
+ function BuyButton({ package: pkg }) {
413
+ const { purchase, isPurchasing, error } = usePurchase();
414
+
415
+ const handlePurchase = async () => {
416
+ try {
417
+ const result = await purchase(pkg);
418
+ console.log('Purchase successful:', result);
419
+ // Show success message
420
+ } catch (err) {
421
+ console.error('Purchase failed:', err);
422
+ // Show error message
423
+ }
424
+ };
425
+
426
+ return (
427
+ <Button onPress={handlePurchase} disabled={isPurchasing}>
428
+ {isPurchasing ? 'Processing...' : `Buy ${pkg.product.priceString}`}
429
+ </Button>
430
+ );
431
+ }
432
+ ```
433
+
434
+ **Returns:**
435
+ ```typescript
436
+ {
437
+ purchase: (pkg: PurchasesPackage) => Promise<PurchaseResult>;
438
+ isPurchasing: boolean;
439
+ error: PurchaseError | null;
440
+ }
441
+ ```
442
+
443
+ #### `useRestorePurchases()`
444
+
445
+ Restore previous purchases.
446
+
447
+ ```typescript
448
+ import { useRestorePurchases } from '@ridwan-retainer/paywall';
449
+
450
+ function RestoreButton() {
451
+ const { restore, isRestoring, error } = useRestorePurchases();
452
+
453
+ const handleRestore = async () => {
454
+ try {
455
+ const result = await restore();
456
+ if (result.customerInfo.activeSubscriptions.length > 0) {
457
+ Alert.alert('Success', 'Purchases restored!');
458
+ } else {
459
+ Alert.alert('No Purchases', 'No purchases found to restore.');
460
+ }
461
+ } catch (err) {
462
+ Alert.alert('Error', 'Failed to restore purchases');
463
+ }
464
+ };
465
+
466
+ return (
467
+ <Button onPress={handleRestore} disabled={isRestoring}>
468
+ {isRestoring ? 'Restoring...' : 'Restore Purchases'}
469
+ </Button>
470
+ );
471
+ }
472
+ ```
473
+
474
+ **Returns:**
475
+ ```typescript
476
+ {
477
+ restore: () => Promise<RestoreResult>;
478
+ isRestoring: boolean;
479
+ error: RestoreError | null;
480
+ }
481
+ ```
482
+
483
+ #### `usePaywall()`
484
+
485
+ Access paywall controller for programmatic control.
486
+
487
+ ```typescript
488
+ import { usePaywall } from '@ridwan-retainer/paywall';
489
+
490
+ function CustomPaywall() {
491
+ const { showPaywall, dismissPaywall } = usePaywall();
492
+
493
+ const handleShowPaywall = () => {
494
+ showPaywall({
495
+ onPurchaseCompleted: (info) => {
496
+ console.log('Purchase completed:', info);
497
+ dismissPaywall();
498
+ },
499
+ onDismiss: () => {
500
+ console.log('Paywall dismissed');
501
+ },
502
+ });
503
+ };
504
+
505
+ return <Button onPress={handleShowPaywall}>Upgrade</Button>;
506
+ }
507
+ ```
508
+
509
+ ### Components
510
+
511
+ #### `<Paywall />`
512
+
513
+ Full-screen RevenueCat paywall UI.
514
+
515
+ ```typescript
516
+ import { Paywall } from '@ridwan-retainer/paywall';
517
+
518
+ <Paywall
519
+ onPurchaseStarted={({ packageBeingPurchased }) => {
520
+ console.log('Starting purchase:', packageBeingPurchased);
521
+ }}
522
+ onPurchaseCompleted={({ customerInfo, storeTransaction }) => {
523
+ console.log('Purchase completed!');
524
+ navigation.goBack();
525
+ }}
526
+ onPurchaseError={({ error }) => {
527
+ Alert.alert('Purchase Failed', error.message);
528
+ }}
529
+ onPurchaseCancelled={() => {
530
+ console.log('User cancelled purchase');
531
+ }}
532
+ onRestoreCompleted={({ customerInfo }) => {
533
+ Alert.alert('Success', 'Purchases restored!');
534
+ }}
535
+ onRestoreError={({ error }) => {
536
+ Alert.alert('Error', 'Restore failed');
537
+ }}
538
+ onDismiss={() => {
539
+ navigation.goBack();
540
+ }}
541
+ />
542
+ ```
543
+
544
+ **Props:**
545
+ ```typescript
546
+ {
547
+ onPurchaseStarted?: (data: { packageBeingPurchased: PurchasesPackage }) => void;
548
+ onPurchaseCompleted?: (data: { customerInfo: CustomerInfo; storeTransaction: any }) => void;
549
+ onPurchaseError?: (data: { error: PurchasesError }) => void;
550
+ onPurchaseCancelled?: () => void;
551
+ onRestoreCompleted?: (data: { customerInfo: CustomerInfo }) => void;
552
+ onRestoreError?: (data: { error: PurchasesError }) => void;
553
+ onDismiss?: () => void;
554
+ }
555
+ ```
556
+
557
+ #### `<SubscriptionGate />`
558
+
559
+ Show content only to subscribers.
560
+
561
+ ```typescript
562
+ import { SubscriptionGate } from '@ridwan-retainer/paywall';
563
+
564
+ <SubscriptionGate>
565
+ <PremiumContent />
566
+ </SubscriptionGate>
567
+
568
+ // Automatically redirects non-subscribers to paywall
569
+ ```
570
+
571
+ #### `<PaywallGate />`
572
+
573
+ Control feature access with modal paywall.
574
+
575
+ ```typescript
576
+ import { PaywallGate } from '@ridwan-retainer/paywall';
577
+
578
+ <PaywallGate
579
+ entitlementId="premium"
580
+ onAccessGranted={() => {
581
+ console.log('User now has access!');
582
+ }}
583
+ >
584
+ <View>
585
+ <Text>Premium Feature</Text>
586
+ <Button title="Click to Access" />
587
+ </View>
588
+ </PaywallGate>
589
+ ```
590
+
591
+ **Props:**
592
+ ```typescript
593
+ {
594
+ entitlementId: string;
595
+ children: ReactNode;
596
+ onAccessGranted?: () => void;
597
+ }
598
+ ```
599
+
600
+ #### `<PurchaseButton />`
601
+
602
+ Pre-styled purchase button.
603
+
604
+ ```typescript
605
+ import { PurchaseButton } from '@ridwan-retainer/paywall';
606
+
607
+ function SubscriptionPlan({ package: pkg }) {
608
+ return (
609
+ <PurchaseButton
610
+ package={pkg}
611
+ title={`Subscribe for ${pkg.product.priceString}`}
612
+ onSuccess={() => {
613
+ Alert.alert('Welcome!', 'You are now subscribed!');
614
+ }}
615
+ onError={(error) => {
616
+ Alert.alert('Error', error.message);
617
+ }}
618
+ style={{ backgroundColor: '#007AFF', padding: 16 }}
619
+ />
620
+ );
621
+ }
622
+ ```
623
+
624
+ **Props:**
625
+ ```typescript
626
+ {
627
+ package: PurchasesPackage;
628
+ onSuccess?: () => void;
629
+ onError?: (error: Error) => void;
630
+ title?: string;
631
+ style?: ViewStyle;
632
+ }
633
+ ```
634
+
635
+ #### `<RestoreButton />`
636
+
637
+ Pre-styled restore purchases button.
638
+
639
+ ```typescript
640
+ import { RestoreButton } from '@ridwan-retainer/paywall';
641
+
642
+ <RestoreButton
643
+ onSuccess={() => {
644
+ Alert.alert('Success', 'Purchases restored!');
645
+ }}
646
+ onError={(error) => {
647
+ Alert.alert('Error', error.message);
648
+ }}
649
+ title="Restore Purchases"
650
+ style={{ padding: 12 }}
651
+ />
652
+ ```
653
+
654
+ **Props:**
655
+ ```typescript
656
+ {
657
+ onSuccess?: () => void;
658
+ onError?: (error: Error) => void;
659
+ title?: string;
660
+ style?: ViewStyle;
661
+ }
662
+ ```
663
+
664
+ #### `<PaywallFooter />`
665
+
666
+ Displays legal text required for App Store compliance.
667
+
668
+ ```typescript
669
+ import { PaywallFooter } from '@ridwan-retainer/paywall';
670
+
671
+ <PaywallFooter
672
+ termsUrl="https://yourapp.com/terms"
673
+ privacyUrl="https://yourapp.com/privacy"
674
+ />
675
+ ```
676
+
677
+ ### Managers
678
+
679
+ For advanced use cases, you can use managers directly:
680
+
681
+ #### `offeringsManager`
682
+
683
+ ```typescript
684
+ import { offeringsManager } from '@ridwan-retainer/paywall';
685
+
686
+ // Fetch offerings
687
+ const offerings = await offeringsManager.fetchOfferings();
688
+
689
+ // Get cached offerings
690
+ const cached = offeringsManager.getCachedOfferings();
691
+ ```
692
+
693
+ #### `customerInfoManager`
694
+
695
+ ```typescript
696
+ import { customerInfoManager } from '@ridwan-retainer/paywall';
697
+
698
+ // Get customer info
699
+ const info = await customerInfoManager.getCustomerInfo();
700
+
701
+ // Check entitlement
702
+ const hasAccess = await customerInfoManager.hasEntitlement('premium');
703
+ ```
704
+
705
+ #### `purchaseManager`
706
+
707
+ ```typescript
708
+ import { purchaseManager } from '@ridwan-retainer/paywall';
709
+
710
+ // Make purchase
711
+ const result = await purchaseManager.purchasePackage(package);
712
+ ```
713
+
714
+ #### `restoreManager`
715
+
716
+ ```typescript
717
+ import { restoreManager } from '@ridwan-retainer/paywall';
718
+
719
+ // Restore purchases
720
+ const result = await restoreManager.restorePurchases();
721
+ ```
722
+
723
+ ## 💡 Complete Examples
724
+
725
+ ### Example 1: Subscription Paywall Screen
726
+
727
+ ```typescript
728
+ import React from 'react';
729
+ import { View, Text, ScrollView, StyleSheet } from 'react-native';
730
+ import {
731
+ useCurrentOffering,
732
+ PurchaseButton,
733
+ RestoreButton,
734
+ PaywallFooter,
735
+ } from '@ridwan-retainer/paywall';
736
+
737
+ function SubscriptionScreen() {
738
+ const { offering, isLoading } = useCurrentOffering();
739
+
740
+ if (isLoading) {
741
+ return <LoadingSpinner />;
742
+ }
743
+
744
+ if (!offering) {
745
+ return <ErrorView message="No plans available" />;
746
+ }
747
+
748
+ return (
749
+ <ScrollView style={styles.container}>
750
+ <Text style={styles.title}>Upgrade to Premium</Text>
751
+ <Text style={styles.subtitle}>
752
+ Unlock all features and support development
753
+ </Text>
754
+
755
+ <View style={styles.features}>
756
+ <FeatureRow icon="✓" text="Unlimited access" />
757
+ <FeatureRow icon="✓" text="Priority support" />
758
+ <FeatureRow icon="✓" text="Remove ads" />
759
+ <FeatureRow icon="✓" text="Cloud sync" />
760
+ </View>
761
+
762
+ {offering.availablePackages.map((pkg) => (
763
+ <View key={pkg.identifier} style={styles.plan}>
764
+ <Text style={styles.planTitle}>{pkg.product.title}</Text>
765
+ <Text style={styles.planPrice}>{pkg.product.priceString}</Text>
766
+ <PurchaseButton
767
+ package={pkg}
768
+ title="Subscribe"
769
+ style={styles.button}
770
+ onSuccess={() => {
771
+ navigation.goBack();
772
+ }}
773
+ />
774
+ </View>
775
+ ))}
776
+
777
+ <RestoreButton
778
+ title="Restore Purchases"
779
+ style={styles.restoreButton}
780
+ />
781
+
782
+ <PaywallFooter
783
+ termsUrl="https://yourapp.com/terms"
784
+ privacyUrl="https://yourapp.com/privacy"
785
+ />
786
+ </ScrollView>
787
+ );
788
+ }
789
+
790
+ const styles = StyleSheet.create({
791
+ container: { flex: 1, padding: 20 },
792
+ title: { fontSize: 28, fontWeight: 'bold', textAlign: 'center' },
793
+ subtitle: { fontSize: 16, color: '#666', textAlign: 'center', marginTop: 8 },
794
+ features: { marginVertical: 24 },
795
+ plan: { marginVertical: 12, padding: 16, borderWidth: 1, borderRadius: 8 },
796
+ button: { backgroundColor: '#007AFF', padding: 16, borderRadius: 8, marginTop: 12 },
797
+ restoreButton: { marginTop: 16, padding: 12 },
798
+ });
799
+ ```
800
+
801
+ ### Example 2: Feature Gating
802
+
803
+ ```typescript
804
+ import { useEntitlement } from '@ridwan-retainer/paywall';
805
+
806
+ function AdvancedSettings() {
807
+ const { hasEntitlement, isLoading } = useEntitlement('premium');
808
+
809
+ if (isLoading) return <Loading />;
810
+
811
+ return (
812
+ <View>
813
+ <SettingItem title="Basic Setting" />
814
+ <SettingItem title="Another Setting" />
815
+
816
+ {hasEntitlement ? (
817
+ <>
818
+ <SettingItem title="Premium Setting 1" />
819
+ <SettingItem title="Premium Setting 2" />
820
+ <SettingItem title="Advanced Options" />
821
+ </>
822
+ ) : (
823
+ <UpgradePrompt
824
+ message="Unlock advanced settings with Premium"
825
+ onPress={() => navigation.navigate('Subscription')}
826
+ />
827
+ )}
828
+ </View>
829
+ );
830
+ }
831
+ ```
832
+
833
+ ### Example 3: Usage-Based Limits
834
+
835
+ ```typescript
836
+ import { useCustomerInfo } from '@ridwan-retainer/paywall';
837
+ import { useState, useEffect } from 'react';
838
+
839
+ function ExportFeature() {
840
+ const { customerInfo } = useCustomerInfo();
841
+ const [exportCount, setExportCount] = useState(0);
52
842
 
843
+ const isPremium = customerInfo?.entitlements.active['premium']?.isActive;
844
+ const maxExports = isPremium ? Infinity : 3;
845
+ const canExport = exportCount < maxExports;
846
+
847
+ const handleExport = async () => {
848
+ if (!canExport && !isPremium) {
849
+ navigation.navigate('Subscription');
850
+ return;
851
+ }
852
+
853
+ await performExport();
854
+ setExportCount(prev => prev + 1);
855
+ };
856
+
53
857
  return (
54
858
  <View>
55
- {!isSubscribed && <PaywallView />}
56
- {isSubscribed && <Text>Thanks for subscribing!</Text>}
859
+ {!isPremium && (
860
+ <Text>{maxExports - exportCount} exports remaining</Text>
861
+ )}
862
+ <Button
863
+ title="Export"
864
+ onPress={handleExport}
865
+ disabled={!canExport && !isPremium}
866
+ />
57
867
  </View>
58
868
  );
59
869
  }
60
870
  ```
61
871
 
62
- ## License
872
+ ## 🧪 Testing
873
+
874
+ ### Test Purchases
875
+
876
+ #### iOS
877
+ 1. Use Sandbox test users (App Store Connect → Users and Access → Sandbox Testers)
878
+ 2. Sign in with test account on device (Settings → App Store → Sandbox Account)
879
+ 3. Make purchases (won't charge real money)
880
+
881
+ #### Android
882
+ 1. Add test users in Google Play Console
883
+ 2. Use license testing or internal testing track
884
+ 3. Make purchases with test account
885
+
886
+ ### Test in Development
887
+
888
+ ```typescript
889
+ import { initializeRevenueCat } from '@ridwan-retainer/paywall';
890
+
891
+ // Enable debug logging
892
+ await initializeRevenueCat(API_KEY, {
893
+ appUserID: `test_user_${Date.now()}`, // Unique test user
894
+ });
895
+ ```
896
+
897
+ ### Mock for Unit Tests
898
+
899
+ ```typescript
900
+ jest.mock('@ridwan-retainer/paywall', () => ({
901
+ useCustomerInfo: () => ({
902
+ customerInfo: {
903
+ entitlements: {
904
+ active: {
905
+ premium: { isActive: true },
906
+ },
907
+ },
908
+ },
909
+ isLoading: false,
910
+ }),
911
+ }));
912
+ ```
913
+
914
+ ## ✅ Best Practices
915
+
916
+ ### 1. Check Subscription Status Regularly
917
+
918
+ ```typescript
919
+ // On app open or resume
920
+ useEffect(() => {
921
+ customerInfoManager.getCustomerInfo({ forceRefresh: true });
922
+ }, []);
923
+ ```
924
+
925
+ ### 2. Handle Errors Gracefully
926
+
927
+ ```typescript
928
+ const { purchase, error } = usePurchase();
929
+
930
+ useEffect(() => {
931
+ if (error) {
932
+ if (error.userCancelled) {
933
+ // User cancelled, no need to show error
934
+ return;
935
+ }
936
+ Alert.alert('Purchase Error', getUserFriendlyErrorMessage(error));
937
+ }
938
+ }, [error]);
939
+ ```
940
+
941
+ ### 3. Provide Restore Option
942
+
943
+ Always show a "Restore Purchases" button for users who reinstalled the app.
944
+
945
+ ### 4. Cache Appropriately
946
+
947
+ The library caches offerings and customer info automatically, but you can force refresh:
948
+
949
+ ```typescript
950
+ const { refresh } = useCustomerInfo();
951
+
952
+ // Force refresh on pull-to-refresh
953
+ <ScrollView
954
+ refreshControl={
955
+ <RefreshControl refreshing={isLoading} onRefresh={refresh} />
956
+ }
957
+ >
958
+ ```
959
+
960
+ ### 5. Test Both Platforms
961
+
962
+ Subscription behavior differs between iOS and Android. Test thoroughly on both.
963
+
964
+ ### 6. Follow App Store Guidelines
965
+
966
+ - Show clear pricing
967
+ - Include terms and privacy links
968
+ - Provide restore functionality
969
+ - Handle subscription management
970
+
971
+ ## 🐛 Troubleshooting
972
+
973
+ ### Issue: "Unable to find offerings"
974
+
975
+ **Solution**: Check RevenueCat dashboard configuration:
976
+ 1. Verify offerings are created
977
+ 2. Check products are added to offerings
978
+ 3. Ensure API keys are correct
979
+
980
+ ### Issue: "Invalid product identifiers"
981
+
982
+ **Solution**: Product IDs must match exactly between:
983
+ - App Store Connect / Google Play Console
984
+ - RevenueCat dashboard
985
+ - Your code
986
+
987
+ ### Issue: Purchases not working in production
988
+
989
+ **Solution**:
990
+ 1. Use production API keys (not sandbox)
991
+ 2. Verify app is live in stores
992
+ 3. Check RevenueCat integration status
993
+ 4. Test with real Apple/Google account
994
+
995
+ ### Issue: "Could not find customer"
996
+
997
+ **Solution**: Initialize RevenueCat before making any calls:
998
+
999
+ ```typescript
1000
+ const { isInitialized } = useRevenueCatInitialization();
1001
+
1002
+ if (!isInitialized) {
1003
+ return <Loading />;
1004
+ }
1005
+ ```
1006
+
1007
+ ### Issue: Subscription status not updating
1008
+
1009
+ **Solution**: Force refresh customer info:
1010
+
1011
+ ```typescript
1012
+ const { refresh } = useCustomerInfo();
1013
+ await refresh();
1014
+ ```
1015
+
1016
+ ## 🤝 Contributing
1017
+
1018
+ Contributions welcome! Please submit Pull Requests.
1019
+
1020
+ ## 📄 License
1021
+
1022
+ MIT © [Ridwan Hamid](https://github.com/RidwanHamid501)
1023
+
1024
+ ## 🔗 Links
1025
+
1026
+ - [npm Package](https://www.npmjs.com/package/@ridwan-retainer/paywall)
1027
+ - [GitHub Repository](https://github.com/RidwanHamid501/app-factory/tree/main/packages/paywall)
1028
+ - [RevenueCat Documentation](https://docs.revenuecat.com)
1029
+ - [RevenueCat React Native SDK](https://github.com/RevenueCat/react-native-purchases)
1030
+
1031
+ ## 📞 Support
63
1032
 
64
- MIT
1033
+ - [GitHub Issues](https://github.com/RidwanHamid501/app-factory/issues)
1034
+ - [RevenueCat Support](https://community.revenuecat.com)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ridwan-retainer/paywall",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Revenue-first monetization layer using RevenueCat for in-app purchases and subscriptions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",