noboarding 0.1.0-alpha

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.
@@ -0,0 +1,756 @@
1
+ # RevenueCat Integration Guide
2
+
3
+ Complete guide to integrating RevenueCat paywalls with Noboarding for seamless paywall analytics and A/B testing.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Overview](#overview)
8
+ 2. [Prerequisites](#prerequisites)
9
+ 3. [Step 1: Install RevenueCat SDK](#step-1-install-revenuecat-sdk)
10
+ 4. [Step 2: Create Custom Paywall Screen](#step-2-create-custom-paywall-screen)
11
+ 5. [Step 3: Register Custom Screen](#step-3-register-custom-screen)
12
+ 6. [Step 4: Configure Webhooks](#step-4-configure-webhooks)
13
+ 7. [Step 5: Add to Dashboard Flow](#step-5-add-to-dashboard-flow)
14
+ 8. [Step 6: Test & Deploy](#step-6-test--deploy)
15
+ 9. [Analytics & Metrics](#analytics--metrics)
16
+ 10. [Troubleshooting](#troubleshooting)
17
+
18
+ ---
19
+
20
+ ## Overview
21
+
22
+ This integration allows you to:
23
+ - 🎯 **A/B test paywall placement** in your onboarding flow
24
+ - 📊 **Track conversion rates** automatically
25
+ - 💰 **Measure revenue** attributed to onboarding sessions
26
+ - 🔄 **Update flows remotely** without app updates
27
+
28
+ **Architecture:**
29
+ ```
30
+ Mobile App (Custom Screen)
31
+ ↓ (presents paywall)
32
+ RevenueCat SDK
33
+ ↓ (purchase event)
34
+ RevenueCat Backend
35
+ ↓ (webhook)
36
+ Supabase Edge Function
37
+ ↓ (stores & attributes)
38
+ Dashboard Analytics
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Prerequisites
44
+
45
+ Before starting, ensure you have:
46
+ - ✅ RevenueCat account ([sign up](https://www.revenuecat.com/))
47
+ - ✅ Products configured in App Store Connect / Google Play Console
48
+ - ✅ Products configured in RevenueCat dashboard
49
+ - ✅ Noboarding SDK installed and configured
50
+ - ✅ Supabase project set up
51
+
52
+ ---
53
+
54
+ ## Step 1: Install RevenueCat SDK
55
+
56
+ ### Install the package
57
+
58
+ ```bash
59
+ npm install react-native-purchases
60
+ # or
61
+ yarn add react-native-purchases
62
+ ```
63
+
64
+ ### iOS Setup
65
+
66
+ Add to your `Podfile`:
67
+ ```ruby
68
+ pod 'RevenueCat'
69
+ ```
70
+
71
+ Then run:
72
+ ```bash
73
+ cd ios && pod install
74
+ ```
75
+
76
+ ### Android Setup
77
+
78
+ No additional setup needed for Android.
79
+
80
+ ### Initialize RevenueCat
81
+
82
+ In your app's root component (usually `App.tsx`):
83
+
84
+ ```typescript
85
+ import { useEffect } from 'react';
86
+ import { Platform } from 'react-native';
87
+ import Purchases from 'react-native-purchases';
88
+
89
+ export default function App() {
90
+ useEffect(() => {
91
+ // Configure RevenueCat
92
+ Purchases.configure({
93
+ apiKey: Platform.OS === 'ios'
94
+ ? 'appl_YOUR_IOS_KEY' // Get from RevenueCat dashboard
95
+ : 'goog_YOUR_ANDROID_KEY',
96
+ });
97
+ }, []);
98
+
99
+ // Rest of your app...
100
+ }
101
+ ```
102
+
103
+ **Where to find API keys:**
104
+ 1. Go to [RevenueCat Dashboard](https://app.revenuecat.com/)
105
+ 2. Select your project
106
+ 3. Go to Settings → API Keys
107
+ 4. Copy iOS and Android keys
108
+
109
+ ### ⚠️ Critical: Sync User IDs
110
+
111
+ **IMPORTANT**: You must use the **same user ID** in both Noboarding SDK and RevenueCat for proper attribution.
112
+
113
+ The Noboarding SDK auto-generates a user ID for analytics. Use the `onUserIdGenerated` callback to sync it with RevenueCat:
114
+
115
+ ```typescript
116
+ import { OnboardingFlow } from 'noboarding';
117
+ import Purchases from 'react-native-purchases';
118
+ import { PaywallScreen } from './screens/PaywallScreen';
119
+
120
+ export default function App() {
121
+ const [showOnboarding, setShowOnboarding] = useState(true);
122
+
123
+ if (showOnboarding) {
124
+ return (
125
+ <OnboardingFlow
126
+ apiKey="sk_live_your_api_key"
127
+ customComponents={{
128
+ PaywallScreen: PaywallScreen,
129
+ }}
130
+ // CRITICAL: Sync user ID with RevenueCat
131
+ onUserIdGenerated={(userId) => {
132
+ console.log('SDK User ID:', userId);
133
+ // Use the SAME ID in RevenueCat
134
+ Purchases.logIn(userId);
135
+ }}
136
+ onComplete={(userData) => {
137
+ console.log('Onboarding complete:', userData);
138
+ setShowOnboarding(false);
139
+ }}
140
+ />
141
+ );
142
+ }
143
+
144
+ return <YourMainApp />;
145
+ }
146
+ ```
147
+
148
+ **Why this matters:**
149
+ - When a user purchases, RevenueCat sends `app_user_id` in the webhook
150
+ - Your backend looks up analytics events by `user_id`
151
+ - If the IDs don't match, conversions can't be attributed to onboarding sessions
152
+ - A/B test metrics will be incomplete
153
+
154
+ ---
155
+
156
+ ## Step 2: Create Custom Paywall Screen
157
+
158
+ Create a new file `screens/PaywallScreen.tsx`:
159
+
160
+ ```typescript
161
+ import React, { useState, useEffect } from 'react';
162
+ import {
163
+ View,
164
+ Text,
165
+ TouchableOpacity,
166
+ ActivityIndicator,
167
+ StyleSheet,
168
+ Alert,
169
+ } from 'react-native';
170
+ import Purchases, { PurchasesOffering, PurchasesPackage } from 'react-native-purchases';
171
+ import type { CustomScreenProps } from 'noboarding';
172
+
173
+ export const PaywallScreen: React.FC<CustomScreenProps> = ({
174
+ analytics,
175
+ onNext,
176
+ onSkip,
177
+ preview,
178
+ onDataUpdate,
179
+ }) => {
180
+ const [loading, setLoading] = useState(true);
181
+ const [purchasing, setPurchasing] = useState(false);
182
+ const [offering, setOffering] = useState<PurchasesOffering | null>(null);
183
+
184
+ useEffect(() => {
185
+ analytics.track('paywall_viewed');
186
+ loadOffering();
187
+ }, []);
188
+
189
+ const loadOffering = async () => {
190
+ try {
191
+ const offerings = await Purchases.getOfferings();
192
+ setOffering(offerings.current);
193
+
194
+ analytics.track('paywall_loaded', {
195
+ offering_id: offerings.current?.identifier,
196
+ packages_count: offerings.current?.availablePackages.length,
197
+ });
198
+ } catch (error: any) {
199
+ analytics.track('paywall_error', { error: error.message });
200
+ } finally {
201
+ setLoading(false);
202
+ }
203
+ };
204
+
205
+ const handlePurchase = async (pkg: PurchasesPackage) => {
206
+ try {
207
+ setPurchasing(true);
208
+
209
+ analytics.track('paywall_purchase_started', {
210
+ package_id: pkg.identifier,
211
+ });
212
+
213
+ const { customerInfo } = await Purchases.purchasePackage(pkg);
214
+ const isPremium = customerInfo.entitlements.active['premium'] !== undefined;
215
+
216
+ if (isPremium) {
217
+ analytics.track('paywall_conversion', {
218
+ package_id: pkg.identifier,
219
+ price: pkg.product.priceString,
220
+ });
221
+
222
+ onDataUpdate?.({
223
+ premium: true,
224
+ package_id: pkg.identifier,
225
+ purchase_timestamp: new Date().toISOString(),
226
+ });
227
+
228
+ Alert.alert('Welcome to Premium!', '', [
229
+ { text: 'Continue', onPress: onNext }
230
+ ]);
231
+ }
232
+ } catch (error: any) {
233
+ const cancelled = error.userCancelled;
234
+
235
+ analytics.track('paywall_purchase_failed', {
236
+ package_id: pkg.identifier,
237
+ cancelled,
238
+ });
239
+
240
+ if (!cancelled) {
241
+ Alert.alert('Purchase Failed', 'Please try again.');
242
+ }
243
+ } finally {
244
+ setPurchasing(false);
245
+ }
246
+ };
247
+
248
+ const handleSkip = () => {
249
+ analytics.track('paywall_dismissed');
250
+ onSkip?.() || onNext();
251
+ };
252
+
253
+ // PREVIEW MODE (for dashboard)
254
+ if (preview) {
255
+ return (
256
+ <View style={styles.previewContainer}>
257
+ <Text style={styles.previewEmoji}>💎</Text>
258
+ <Text style={styles.previewTitle}>Paywall Preview</Text>
259
+ <Text style={styles.previewNote}>(Real paywall in app)</Text>
260
+ <TouchableOpacity style={styles.button} onPress={onNext}>
261
+ <Text style={styles.buttonText}>Continue</Text>
262
+ </TouchableOpacity>
263
+ </View>
264
+ );
265
+ }
266
+
267
+ if (loading) {
268
+ return (
269
+ <View style={styles.centerContainer}>
270
+ <ActivityIndicator size="large" color="#007AFF" />
271
+ <Text style={styles.loadingText}>Loading options...</Text>
272
+ </View>
273
+ );
274
+ }
275
+
276
+ return (
277
+ <View style={styles.container}>
278
+ <Text style={styles.title}>Unlock Premium</Text>
279
+ <Text style={styles.subtitle}>Get full access to all features</Text>
280
+
281
+ {offering?.availablePackages.map((pkg) => (
282
+ <TouchableOpacity
283
+ key={pkg.identifier}
284
+ style={styles.packageCard}
285
+ onPress={() => handlePurchase(pkg)}
286
+ disabled={purchasing}
287
+ >
288
+ <Text style={styles.packageTitle}>
289
+ {pkg.product.title?.replace(/\(.*?\)/, '').trim()}
290
+ </Text>
291
+ <Text style={styles.packagePrice}>{pkg.product.priceString}</Text>
292
+ </TouchableOpacity>
293
+ ))}
294
+
295
+ {purchasing && (
296
+ <View style={styles.purchasingOverlay}>
297
+ <ActivityIndicator size="large" color="#FFFFFF" />
298
+ <Text style={styles.purchasingText}>Processing...</Text>
299
+ </View>
300
+ )}
301
+
302
+ <TouchableOpacity onPress={handleSkip} style={styles.skipButton}>
303
+ <Text style={styles.skipText}>Maybe Later</Text>
304
+ </TouchableOpacity>
305
+ </View>
306
+ );
307
+ };
308
+
309
+ const styles = StyleSheet.create({
310
+ container: {
311
+ flex: 1,
312
+ padding: 20,
313
+ backgroundColor: '#FFFFFF',
314
+ },
315
+ centerContainer: {
316
+ flex: 1,
317
+ justifyContent: 'center',
318
+ alignItems: 'center',
319
+ },
320
+ previewContainer: {
321
+ flex: 1,
322
+ justifyContent: 'center',
323
+ alignItems: 'center',
324
+ padding: 20,
325
+ backgroundColor: '#F5F5F5',
326
+ },
327
+ previewEmoji: {
328
+ fontSize: 64,
329
+ marginBottom: 16,
330
+ },
331
+ previewTitle: {
332
+ fontSize: 24,
333
+ fontWeight: 'bold',
334
+ marginBottom: 8,
335
+ },
336
+ previewNote: {
337
+ fontSize: 14,
338
+ color: '#999',
339
+ marginBottom: 24,
340
+ },
341
+ title: {
342
+ fontSize: 32,
343
+ fontWeight: 'bold',
344
+ marginBottom: 8,
345
+ textAlign: 'center',
346
+ },
347
+ subtitle: {
348
+ fontSize: 18,
349
+ color: '#666',
350
+ marginBottom: 32,
351
+ textAlign: 'center',
352
+ },
353
+ loadingText: {
354
+ marginTop: 16,
355
+ fontSize: 16,
356
+ color: '#666',
357
+ },
358
+ packageCard: {
359
+ backgroundColor: '#F5F5F5',
360
+ borderRadius: 12,
361
+ padding: 20,
362
+ marginBottom: 12,
363
+ },
364
+ packageTitle: {
365
+ fontSize: 18,
366
+ fontWeight: '600',
367
+ marginBottom: 8,
368
+ },
369
+ packagePrice: {
370
+ fontSize: 24,
371
+ fontWeight: 'bold',
372
+ color: '#007AFF',
373
+ },
374
+ button: {
375
+ backgroundColor: '#007AFF',
376
+ borderRadius: 12,
377
+ padding: 16,
378
+ alignItems: 'center',
379
+ },
380
+ buttonText: {
381
+ color: '#FFFFFF',
382
+ fontSize: 18,
383
+ fontWeight: '600',
384
+ },
385
+ skipButton: {
386
+ marginTop: 16,
387
+ padding: 12,
388
+ alignItems: 'center',
389
+ },
390
+ skipText: {
391
+ fontSize: 16,
392
+ color: '#666',
393
+ },
394
+ purchasingOverlay: {
395
+ ...StyleSheet.absoluteFillObject,
396
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
397
+ justifyContent: 'center',
398
+ alignItems: 'center',
399
+ },
400
+ purchasingText: {
401
+ color: '#FFFFFF',
402
+ marginTop: 16,
403
+ fontSize: 16,
404
+ },
405
+ });
406
+ ```
407
+
408
+ ---
409
+
410
+ ## Step 3: Register Custom Screen
411
+
412
+ In your app's `App.tsx`, register the paywall screen:
413
+
414
+ ```typescript
415
+ import { OnboardingFlow } from 'noboarding';
416
+ import { PaywallScreen } from './screens/PaywallScreen';
417
+ import Purchases from 'react-native-purchases';
418
+
419
+ export default function App() {
420
+ const [showOnboarding, setShowOnboarding] = useState(true);
421
+
422
+ if (showOnboarding) {
423
+ return (
424
+ <OnboardingFlow
425
+ apiKey="sk_live_your_api_key"
426
+ customComponents={{
427
+ PaywallScreen: PaywallScreen, // Register here
428
+ }}
429
+ // CRITICAL: Sync user ID with RevenueCat
430
+ onUserIdGenerated={(userId) => {
431
+ Purchases.logIn(userId);
432
+ }}
433
+ onComplete={(userData) => {
434
+ console.log('Onboarding complete:', userData);
435
+ // userData.premium will be true if they purchased
436
+ setShowOnboarding(false);
437
+ }}
438
+ />
439
+ );
440
+ }
441
+
442
+ return <YourMainApp />;
443
+ }
444
+ ```
445
+
446
+ ---
447
+
448
+ ## Step 4: Configure Webhooks
449
+
450
+ ### 4.1 Deploy Supabase Migration
451
+
452
+ Run the migration to create the `revenuecat_events` table:
453
+
454
+ ```bash
455
+ cd supabase
456
+ supabase db push
457
+ ```
458
+
459
+ This creates the table defined in `migrations/20260219100000_add_revenuecat_events.sql`.
460
+
461
+ ### 4.2 Deploy Edge Function
462
+
463
+ Deploy the webhook handler:
464
+
465
+ ```bash
466
+ supabase functions deploy revenuecat-webhook
467
+ ```
468
+
469
+ ### 4.3 Set Webhook Secret
470
+
471
+ ```bash
472
+ supabase secrets set REVENUECAT_WEBHOOK_SECRET=your_random_secret_here
473
+ ```
474
+
475
+ Generate a secure secret:
476
+ ```bash
477
+ openssl rand -base64 32
478
+ ```
479
+
480
+ ### 4.4 Configure RevenueCat Webhook
481
+
482
+ 1. Go to [RevenueCat Dashboard](https://app.revenuecat.com/)
483
+ 2. Navigate to **Integrations → Webhooks**
484
+ 3. Click **Add New Webhook**
485
+ 4. Configure:
486
+ - **URL:** `https://YOUR_PROJECT.supabase.co/functions/v1/revenuecat-webhook`
487
+ - **Authorization Header:** `Bearer your_random_secret_here`
488
+ - **Events to send:** Select at minimum:
489
+ - ✅ INITIAL_PURCHASE
490
+ - ✅ RENEWAL
491
+ - ✅ CANCELLATION
492
+ - ✅ BILLING_ISSUE
493
+ 5. Click **Save**
494
+
495
+ ### 4.5 Test Webhook
496
+
497
+ RevenueCat provides a test button. Click it to send a test event. Check your Supabase logs:
498
+
499
+ ```bash
500
+ supabase functions logs revenuecat-webhook --tail
501
+ ```
502
+
503
+ You should see:
504
+ ```
505
+ Received RevenueCat event: { type: 'TEST', app_user_id: '...', ... }
506
+ ```
507
+
508
+ ---
509
+
510
+ ## Step 5: Add to Dashboard Flow
511
+
512
+ 1. **Log into your dashboard**
513
+ 2. **Go to Flows** and select or create a flow
514
+ 3. **Click "Add Custom Screen"**
515
+ 4. **Enter details:**
516
+ - **Component Name:** `PaywallScreen` (must match exactly)
517
+ - **Description:** "Premium subscription paywall"
518
+ 5. **Position the screen** where you want it in the flow
519
+ 6. **Save Draft**
520
+
521
+ **Example flow:**
522
+ ```
523
+ 1. Welcome Screen (SDK)
524
+ 2. Feature Tour (SDK)
525
+ 3. PaywallScreen (Custom) ← Your paywall
526
+ 4. Setup Complete (SDK)
527
+ ```
528
+
529
+ ---
530
+
531
+ ## Step 6: Test & Deploy
532
+
533
+ ### Local Testing
534
+
535
+ 1. **Test in your app:**
536
+ ```bash
537
+ npm start
538
+ # or
539
+ yarn start
540
+ ```
541
+
542
+ 2. **Navigate to paywall screen**
543
+ 3. **Use RevenueCat sandbox mode** for testing:
544
+ - iOS: Use sandbox Apple ID
545
+ - Android: Use test Google account
546
+
547
+ ### Production Deployment
548
+
549
+ 1. **Build your app:**
550
+ ```bash
551
+ # iOS
552
+ npx react-native run-ios --configuration Release
553
+
554
+ # Android
555
+ npx react-native run-android --variant=release
556
+ ```
557
+
558
+ 2. **Submit to app stores**
559
+ 3. **Wait for approval** (1-3 days typically)
560
+ 4. **Publish your flow** in the dashboard after app is live
561
+
562
+ ---
563
+
564
+ ## Analytics & Metrics
565
+
566
+ Once configured, you'll automatically track:
567
+
568
+ ### Client-Side Events (from SDK)
569
+ - `paywall_viewed` - User saw the paywall
570
+ - `paywall_loaded` - Offerings loaded successfully
571
+ - `paywall_purchase_started` - User tapped purchase
572
+ - `paywall_conversion` - Purchase completed
573
+ - `paywall_purchase_failed` - Purchase failed
574
+ - `paywall_dismissed` - User skipped paywall
575
+
576
+ ### Server-Side Events (from webhook)
577
+ - `paywall_conversion` - Verified purchase from RevenueCat
578
+ - Includes: `product_id`, `price`, `currency`, `transaction_id`
579
+
580
+ ### Dashboard Metrics
581
+
582
+ **Analytics Page:**
583
+ - 💎 Paywall Views
584
+ - 💳 Conversions
585
+ - 📊 Conversion Rate
586
+ - 💰 Total Revenue
587
+
588
+ **A/B Tests:**
589
+ - Compare paywall placement
590
+ - Test different offerings
591
+ - Measure impact on conversion
592
+
593
+ ### Example Query
594
+
595
+ Get conversion rate by variant:
596
+
597
+ ```sql
598
+ SELECT
599
+ variant_id,
600
+ COUNT(DISTINCT CASE WHEN event_name = 'paywall_viewed' THEN user_id END) as views,
601
+ COUNT(DISTINCT CASE WHEN event_name = 'paywall_conversion' THEN user_id END) as conversions,
602
+ ROUND(
603
+ 100.0 * COUNT(DISTINCT CASE WHEN event_name = 'paywall_conversion' THEN user_id END) /
604
+ NULLIF(COUNT(DISTINCT CASE WHEN event_name = 'paywall_viewed' THEN user_id END), 0),
605
+ 2
606
+ ) as conversion_rate
607
+ FROM analytics_events
608
+ WHERE experiment_id = 'your_experiment_id'
609
+ GROUP BY variant_id;
610
+ ```
611
+
612
+ ---
613
+
614
+ ## Troubleshooting
615
+
616
+ ### Paywall not showing
617
+
618
+ **Check:**
619
+ 1. Component is registered in `customComponents`
620
+ 2. Name matches exactly (case-sensitive): `PaywallScreen`
621
+ 3. App version includes the custom screen
622
+ 4. Dashboard flow is published
623
+
624
+ **Debug:**
625
+ ```typescript
626
+ customComponents={{
627
+ PaywallScreen: PaywallScreen,
628
+ }}
629
+
630
+ console.log('Registered:', Object.keys(customComponents));
631
+ ```
632
+
633
+ ### No products available
634
+
635
+ **Check:**
636
+ 1. Products configured in App Store Connect / Google Play
637
+ 2. Products added to RevenueCat dashboard
638
+ 3. Products added to an offering
639
+ 4. Using correct entitlement ID
640
+
641
+ **Debug:**
642
+ ```typescript
643
+ const offerings = await Purchases.getOfferings();
644
+ console.log('Current offering:', offerings.current);
645
+ console.log('Packages:', offerings.current?.availablePackages);
646
+ ```
647
+
648
+ ### Webhook not receiving events
649
+
650
+ **Check:**
651
+ 1. URL is correct: `https://YOUR_PROJECT.supabase.co/functions/v1/revenuecat-webhook`
652
+ 2. Authorization header is set
653
+ 3. Events are selected in RevenueCat
654
+ 4. Function is deployed: `supabase functions list`
655
+
656
+ **Debug:**
657
+ ```bash
658
+ # View function logs
659
+ supabase functions logs revenuecat-webhook --tail
660
+
661
+ # Test webhook manually
662
+ curl -X POST https://YOUR_PROJECT.supabase.co/functions/v1/revenuecat-webhook \
663
+ -H "Authorization: Bearer your_secret" \
664
+ -H "Content-Type: application/json" \
665
+ -d '{"api_version":"1.0","event":{"id":"test","type":"TEST","app_user_id":"test_user","product_id":"test"}}'
666
+ ```
667
+
668
+ ### Conversions not attributed to sessions
669
+
670
+ **Check:**
671
+ 1. `app_user_id` in RevenueCat matches `user_id` in analytics events
672
+ 2. Purchase happens within 24 hours of onboarding session
673
+ 3. Webhook secret is correct
674
+
675
+ **Debug:**
676
+ ```sql
677
+ -- Check if user has analytics events
678
+ SELECT * FROM analytics_events
679
+ WHERE user_id = 'your_app_user_id'
680
+ ORDER BY timestamp DESC;
681
+
682
+ -- Check if purchase was recorded
683
+ SELECT * FROM revenuecat_events
684
+ WHERE app_user_id = 'your_app_user_id'
685
+ ORDER BY purchased_at DESC;
686
+ ```
687
+
688
+ ### Revenue not showing
689
+
690
+ **Check:**
691
+ 1. `price` and `currency` fields are present in webhook payload
692
+ 2. Event type is `INITIAL_PURCHASE`
693
+ 3. Migration created `revenuecat_events` table
694
+
695
+ **Debug:**
696
+ ```sql
697
+ -- Check raw webhook data
698
+ SELECT
699
+ event_type,
700
+ product_id,
701
+ price,
702
+ currency,
703
+ raw_payload
704
+ FROM revenuecat_events
705
+ ORDER BY purchased_at DESC
706
+ LIMIT 10;
707
+ ```
708
+
709
+ ---
710
+
711
+ ## Best Practices
712
+
713
+ ### 1. Set User IDs Consistently
714
+
715
+ Ensure the same user ID is used for both:
716
+ - RevenueCat: `Purchases.logIn(userId)`
717
+ - Noboarding analytics: Automatically uses device ID
718
+
719
+ ### 2. Handle Edge Cases
720
+
721
+ - Network failures
722
+ - Purchase cancellations
723
+ - Restore purchases flow
724
+ - Family sharing
725
+
726
+ ### 3. Test Thoroughly
727
+
728
+ - Test sandbox purchases
729
+ - Test webhook delivery
730
+ - Test analytics attribution
731
+ - Test A/B experiments
732
+
733
+ ### 4. Monitor Performance
734
+
735
+ - Track conversion rates
736
+ - Monitor drop-off points
737
+ - A/B test paywall placement
738
+ - Measure revenue impact
739
+
740
+ ---
741
+
742
+ ## Next Steps
743
+
744
+ - 📖 Read the [Custom Screens Guide](./cusomte_screens.md)
745
+ - 🎨 Customize your paywall UI
746
+ - 📊 Set up A/B tests in the dashboard
747
+ - 💡 Experiment with paywall placement
748
+
749
+ **Need help?**
750
+ - RevenueCat Docs: https://www.revenuecat.com/docs
751
+ - Noboarding Support: support@noboarding.com
752
+ - Discord Community: [Join here](#)
753
+
754
+ ---
755
+
756
+ **Happy building! 🚀**