vibefast-cli 0.5.0 → 0.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.
Files changed (77) hide show
  1. package/FEATURE-DEPENDENCY-SPEC.md +338 -0
  2. package/dist/__tests__/integration.test.d.ts +2 -0
  3. package/dist/__tests__/integration.test.d.ts.map +1 -0
  4. package/dist/__tests__/integration.test.js +219 -0
  5. package/dist/__tests__/integration.test.js.map +1 -0
  6. package/dist/__tests__/recipes.test.d.ts +2 -0
  7. package/dist/__tests__/recipes.test.d.ts.map +1 -0
  8. package/dist/__tests__/recipes.test.js +143 -0
  9. package/dist/__tests__/recipes.test.js.map +1 -0
  10. package/dist/commands/__tests__/init.test.d.ts +2 -0
  11. package/dist/commands/__tests__/init.test.d.ts.map +1 -0
  12. package/dist/commands/__tests__/init.test.js +95 -0
  13. package/dist/commands/__tests__/init.test.js.map +1 -0
  14. package/dist/commands/__tests__/platform.test.d.ts +2 -0
  15. package/dist/commands/__tests__/platform.test.d.ts.map +1 -0
  16. package/dist/commands/__tests__/platform.test.js +123 -0
  17. package/dist/commands/__tests__/platform.test.js.map +1 -0
  18. package/dist/commands/add.d.ts.map +1 -1
  19. package/dist/commands/add.js +4 -5
  20. package/dist/commands/add.js.map +1 -1
  21. package/dist/core/journal.d.ts.map +1 -1
  22. package/dist/core/journal.js +36 -19
  23. package/dist/core/journal.js.map +1 -1
  24. package/dist/core/recipes.d.ts.map +1 -1
  25. package/dist/core/recipes.js +8 -39
  26. package/dist/core/recipes.js.map +1 -1
  27. package/package.json +1 -1
  28. package/recipes/ios-widget/recipe.json +78 -0
  29. package/recipes/ios-widget/targets/widget/AppIntent.swift +46 -0
  30. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png +0 -0
  31. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png +0 -0
  32. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png +0 -0
  33. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png +0 -0
  34. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png +0 -0
  35. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png +0 -0
  36. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png +0 -0
  37. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png +0 -0
  38. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png +0 -0
  39. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png +0 -0
  40. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png +0 -0
  41. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png +0 -0
  42. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png +0 -0
  43. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png +0 -0
  44. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  45. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png +0 -0
  46. package/recipes/ios-widget/targets/widget/CalorieTrackerWidget.swift +424 -0
  47. package/recipes/ios-widget/targets/widget/HabitTrackerWidget.swift +305 -0
  48. package/recipes/ios-widget/targets/widget/Info.plist +11 -0
  49. package/recipes/ios-widget/targets/widget/WidgetLiveActivity.swift +75 -0
  50. package/recipes/ios-widget/targets/widget/expo-target.config.js +10 -0
  51. package/recipes/ios-widget/targets/widget/generated.entitlements +5 -0
  52. package/recipes/ios-widget/targets/widget/index.swift +18 -0
  53. package/recipes/ios-widget/targets/widget/widgets.swift +96 -0
  54. package/recipes/ios-widget@latest.zip +0 -0
  55. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +74 -0
  56. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +25 -0
  57. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +23 -0
  58. package/recipes/payments/apps/native/src/features/payments/README.md +200 -0
  59. package/recipes/payments/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
  60. package/recipes/payments/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
  61. package/recipes/payments/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
  62. package/recipes/payments/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
  63. package/recipes/payments/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
  64. package/recipes/payments/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
  65. package/recipes/payments/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
  66. package/recipes/payments/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
  67. package/recipes/payments/apps/native/src/features/payments/index.ts +8 -0
  68. package/recipes/payments/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
  69. package/recipes/payments/recipe.json +58 -0
  70. package/recipes/payments@latest.zip +0 -0
  71. package/src/__tests__/integration.test.ts +249 -0
  72. package/src/__tests__/recipes.test.ts +168 -0
  73. package/src/commands/__tests__/init.test.ts +112 -0
  74. package/src/commands/__tests__/platform.test.ts +141 -0
  75. package/src/commands/add.ts +4 -5
  76. package/src/core/journal.ts +42 -25
  77. package/src/core/recipes.ts +8 -40
@@ -0,0 +1,200 @@
1
+ # Payments Feature - RevenueCat Integration
2
+
3
+ This feature provides a complete RevenueCat integration for handling in-app purchases and subscriptions in your React Native Expo app.
4
+
5
+ ## Features
6
+
7
+ - ✅ RevenueCat SDK initialization on app startup
8
+ - ✅ Automatic user authentication sync with RevenueCat
9
+ - ✅ Product offerings fetching
10
+ - ✅ Purchase handling (subscriptions and consumable products)
11
+ - ✅ Subscription status checking
12
+ - ✅ Purchase restoration
13
+ - ✅ Complete TypeScript support
14
+ - ✅ Comprehensive error handling
15
+ - ✅ Testing utilities
16
+
17
+ ## Setup
18
+
19
+ ### 1. Environment Variables
20
+
21
+ Make sure your RevenueCat API keys are configured in your `.env.local` file:
22
+
23
+ ```env
24
+ REVENUECAT_API_KEY_APPLE=your_apple_key_here
25
+ REVENUECAT_API_KEY_GOOGLE=your_google_key_here
26
+ ```
27
+
28
+ ### 2. Automatic Initialization
29
+
30
+ The `PaymentInitializer` component is already added to your app's root layout and will:
31
+
32
+ - Initialize RevenueCat SDK on app startup
33
+ - Automatically set user ID when users log in
34
+ - Handle user logout from RevenueCat
35
+
36
+ No additional setup required!
37
+
38
+ ## Usage
39
+
40
+ ### Checking User Entitlements
41
+
42
+ Use the `useEntitlement` hook to check if a user has access to premium features:
43
+
44
+ ```tsx
45
+ import { useEntitlement } from '@/features/payments';
46
+
47
+ export const PremiumFeature = () => {
48
+ const { isEntitled, isLoading, error } = useEntitlement('premium');
49
+
50
+ if (isLoading) {
51
+ return <Text>Checking subscription...</Text>;
52
+ }
53
+
54
+ if (error) {
55
+ return <Text>Error: {error}</Text>;
56
+ }
57
+
58
+ if (!isEntitled) {
59
+ return <PaywallScreen />;
60
+ }
61
+
62
+ return <PremiumContent />;
63
+ };
64
+ ```
65
+
66
+ ### Fetching Product Offerings
67
+
68
+ ```tsx
69
+ import { RevenueCatAdapter } from '@/features/payments';
70
+
71
+ const paymentService = new RevenueCatAdapter();
72
+
73
+ // Fetch available products
74
+ const offerings = await paymentService.fetchProductOfferings('remote');
75
+ console.log('Available products:', offerings);
76
+ ```
77
+
78
+ ### Making Purchases
79
+
80
+ ```tsx
81
+ import { RevenueCatAdapter } from '@/features/payments';
82
+
83
+ const paymentService = new RevenueCatAdapter();
84
+
85
+ // Purchase a subscription
86
+ const result = await paymentService.purchaseProduct('premium_monthly');
87
+
88
+ if (result.success) {
89
+ console.log('Purchase successful!');
90
+ // Handle successful purchase
91
+ } else {
92
+ console.log('Purchase failed:', result.error);
93
+ // Handle error
94
+ }
95
+ ```
96
+
97
+ ### Handling Consumable Products (Credits)
98
+
99
+ For consumable products like credits, provide a callback to record the purchase:
100
+
101
+ ```tsx
102
+ const result = await paymentService.purchaseProduct(
103
+ 'credits_100',
104
+ undefined, // offering identifier (optional)
105
+ async ({ productId, quantity }) => {
106
+ // Record the credit purchase in your backend
107
+ await recordCreditPurchase(productId, quantity);
108
+ },
109
+ );
110
+ ```
111
+
112
+ ### Restoring Purchases
113
+
114
+ ```tsx
115
+ const result = await paymentService.restorePurchases();
116
+
117
+ if (result.success) {
118
+ console.log('Restored entitlements:', result.restoredEntitlements);
119
+ } else {
120
+ console.log('Restore failed:', result.error);
121
+ }
122
+ ```
123
+
124
+ ## Architecture
125
+
126
+ ### Components
127
+
128
+ - **`PaymentInitializer`** - Handles SDK initialization and user auth sync
129
+ - **`useEntitlement`** - Hook for checking subscription status
130
+
131
+ ### Services
132
+
133
+ - **`RevenueCatAdapter`** - Main service implementing the PaymentService interface
134
+ - **`PaymentService`** - Core interface defined in `@/core/payments`
135
+
136
+ ### Key Features
137
+
138
+ 1. **Modular Design** - Uses adapter pattern for easy swapping of payment providers
139
+ 2. **Type Safety** - Full TypeScript support with proper types
140
+ 3. **Error Handling** - Graceful error handling with logging
141
+ 4. **Testing** - Comprehensive test coverage
142
+ 5. **Platform Support** - Works on both iOS and Android
143
+
144
+ ## Error Handling
145
+
146
+ The integration includes comprehensive error handling:
147
+
148
+ - SDK initialization failures won't crash the app
149
+ - Purchase errors are returned as structured responses
150
+ - All errors are logged for debugging
151
+ - Network issues are handled gracefully
152
+
153
+ ## Testing
154
+
155
+ Run the payment feature tests:
156
+
157
+ ```bash
158
+ npm test src/features/payments
159
+ ```
160
+
161
+ ## Configuration
162
+
163
+ ### Product ID Patterns
164
+
165
+ For consumable products (credits), the adapter automatically detects quantities based on product ID patterns:
166
+
167
+ - `credits_100` → 100 credits
168
+ - `100_credits` → 100 credits
169
+ - `credit_50` → 50 credits
170
+
171
+ ### Entitlement IDs
172
+
173
+ Make sure your entitlement identifiers in RevenueCat match what you use in the `useEntitlement` hook.
174
+
175
+ ## Troubleshooting
176
+
177
+ ### Common Issues
178
+
179
+ 1. **SDK Not Initialized Error**
180
+ - Ensure API keys are set in environment variables
181
+ - Check that `PaymentInitializer` is included in app root
182
+
183
+ 2. **Purchase Failures**
184
+ - Verify product IDs match your RevenueCat configuration
185
+ - Check that offerings are properly configured in RevenueCat dashboard
186
+
187
+ 3. **Entitlement Not Found**
188
+ - Ensure entitlement ID matches your RevenueCat configuration
189
+ - Check that user is properly logged in
190
+
191
+ ### Debug Logging
192
+
193
+ In development mode, debug logs are automatically enabled. Check your console for detailed RevenueCat logs.
194
+
195
+ ## Next Steps
196
+
197
+ 1. Configure your products in the RevenueCat dashboard
198
+ 2. Set up your entitlements and offerings
199
+ 3. Test purchases in sandbox mode
200
+ 4. Implement your paywall UI using the provided hooks and services
@@ -0,0 +1,194 @@
1
+ import { useEffect, useState } from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Alert,
5
+ Pressable,
6
+ ScrollView,
7
+ Text,
8
+ View,
9
+ } from 'react-native';
10
+
11
+ import { paymentsApi } from '@/api-client/payments';
12
+ import { FocusAwareStatusBar } from '@/components/ui';
13
+ import type {
14
+ PaymentService,
15
+ ProductOffering,
16
+ } from '@/core/payments/payment-service';
17
+ import { translate } from '@/lib';
18
+ import { useThemeConfig } from '@/lib/use-theme-config';
19
+
20
+ import { PaywallLocalMode } from '../components/paywall-local-mode';
21
+
22
+ interface LocalPaywallProps {
23
+ paymentService: PaymentService;
24
+ }
25
+
26
+ export default function LocalPaywall({ paymentService }: LocalPaywallProps) {
27
+ const theme = useThemeConfig();
28
+ const [offerings, setOfferings] = useState<ProductOffering[]>([]);
29
+ const [isLoading, setIsLoading] = useState(true);
30
+ const [error, setError] = useState<string | null>(null);
31
+ const [isPurchasing, setIsPurchasing] = useState(false);
32
+
33
+ // Convex mutation for recording consumable purchases
34
+ const recordConsumablePurchase = paymentsApi.useRecordConsumablePurchase();
35
+
36
+ useEffect(() => {
37
+ const loadOfferings = async () => {
38
+ try {
39
+ setIsLoading(true);
40
+ setError(null);
41
+ const fetchedOfferings =
42
+ await paymentService.fetchProductOfferings('local');
43
+ setOfferings(fetchedOfferings);
44
+ } catch (e) {
45
+ setError(translate('paywall.errors.load_offerings'));
46
+ console.error('Error fetching offerings:', e);
47
+ } finally {
48
+ setIsLoading(false);
49
+ }
50
+ };
51
+ loadOfferings();
52
+ }, [paymentService]);
53
+
54
+ const handleLocalPurchase = async (productId: string) => {
55
+ try {
56
+ setIsPurchasing(true);
57
+ await paymentService.purchaseProduct(
58
+ productId,
59
+ undefined,
60
+ async (details) => {
61
+ try {
62
+ await recordConsumablePurchase({
63
+ productId: details.productId,
64
+ quantity: details.quantity,
65
+ });
66
+ } catch (error) {
67
+ console.error('Failed to record consumable purchase:', error);
68
+ }
69
+ },
70
+ );
71
+ } catch (e) {
72
+ const errorMessage = String(e);
73
+ if (
74
+ errorMessage.includes('userCancelled') ||
75
+ errorMessage.includes('User cancelled')
76
+ ) {
77
+ return;
78
+ }
79
+ Alert.alert(
80
+ translate('paywall.errors.purchase_failed_title'),
81
+ translate('paywall.errors.purchase_failed_message'),
82
+ );
83
+ console.error('Purchase error:', e);
84
+ } finally {
85
+ setIsPurchasing(false);
86
+ }
87
+ };
88
+
89
+ const handleRestorePurchases = async () => {
90
+ try {
91
+ setIsPurchasing(true);
92
+ const result = await paymentService.restorePurchases();
93
+
94
+ if (result.success) {
95
+ const restoredCount = result.restoredEntitlements?.length ?? 0;
96
+ const message =
97
+ restoredCount > 0
98
+ ? translate('paywall.restore.success_with_count', {
99
+ count: restoredCount,
100
+ })
101
+ : translate('paywall.restore.none');
102
+ Alert.alert(translate('paywall.restore.complete_title'), message);
103
+ } else {
104
+ Alert.alert(
105
+ translate('paywall.restore.failed_title'),
106
+ result.error ?? translate('paywall.restore.failed_message'),
107
+ );
108
+ }
109
+ } catch (e) {
110
+ Alert.alert(
111
+ translate('paywall.restore.failed_title'),
112
+ translate('paywall.restore.error_message'),
113
+ );
114
+ console.error('Restore purchases error:', e);
115
+ } finally {
116
+ setIsPurchasing(false);
117
+ }
118
+ };
119
+
120
+ const retryLoadOfferings = async () => {
121
+ setError(null);
122
+ setIsLoading(true);
123
+ try {
124
+ const fetchedOfferings =
125
+ await paymentService.fetchProductOfferings('local');
126
+ setOfferings(fetchedOfferings);
127
+ } catch {
128
+ setError(translate('paywall.errors.load_offerings'));
129
+ } finally {
130
+ setIsLoading(false);
131
+ }
132
+ };
133
+
134
+ return (
135
+ <>
136
+ <FocusAwareStatusBar />
137
+ <View
138
+ className="flex-1"
139
+ style={{ backgroundColor: theme.colors.background }}
140
+ >
141
+ <ScrollView
142
+ className="flex-1"
143
+ showsVerticalScrollIndicator={false}
144
+ contentContainerStyle={{ paddingTop: 60, paddingBottom: 20 }}
145
+ >
146
+ <PaywallLocalMode
147
+ isLoading={isLoading}
148
+ error={error}
149
+ offerings={offerings}
150
+ onPurchase={handleLocalPurchase}
151
+ onRetry={retryLoadOfferings}
152
+ onRestorePurchases={handleRestorePurchases}
153
+ isPurchasing={isPurchasing}
154
+ primaryColor={theme.colors.primary}
155
+ />
156
+
157
+ {/* Restore Purchases Button */}
158
+ <View className="mx-4 mt-8">
159
+ <Pressable
160
+ onPress={handleRestorePurchases}
161
+ disabled={isPurchasing}
162
+ className="items-center py-3"
163
+ >
164
+ <Text
165
+ className="text-base"
166
+ style={{
167
+ color: theme.colors.mutedForeground,
168
+ opacity: isPurchasing ? 0.5 : 1,
169
+ }}
170
+ >
171
+ {translate('paywall.restore_purchases')}
172
+ </Text>
173
+ </Pressable>
174
+
175
+ {isPurchasing && (
176
+ <View className="mt-2 flex-row items-center justify-center">
177
+ <ActivityIndicator
178
+ size="small"
179
+ color={theme.colors.mutedForeground}
180
+ />
181
+ <Text
182
+ className="ml-2"
183
+ style={{ color: theme.colors.mutedForeground }}
184
+ >
185
+ {translate('paywall.processing')}
186
+ </Text>
187
+ </View>
188
+ )}
189
+ </View>
190
+ </ScrollView>
191
+ </View>
192
+ </>
193
+ );
194
+ }
@@ -0,0 +1,79 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Alert, View } from 'react-native';
3
+ import Purchases, {
4
+ type CustomerInfo,
5
+ type PurchasesOffering,
6
+ } from 'react-native-purchases';
7
+ import RevenueCatUI from 'react-native-purchases-ui';
8
+
9
+ import { FocusAwareStatusBar } from '@/components/ui';
10
+ import { translate } from '@/lib';
11
+ import { useThemeConfig } from '@/lib/use-theme-config';
12
+
13
+ export default function RemotePaywall() {
14
+ const theme = useThemeConfig();
15
+ const [offering, setOffering] = useState<PurchasesOffering | null>(null);
16
+ const [isLoading, setIsLoading] = useState(true);
17
+
18
+ useEffect(() => {
19
+ const loadOfferings = async () => {
20
+ try {
21
+ setIsLoading(true);
22
+ // Get offerings directly from RevenueCat to get the correct type
23
+ const offerings = await Purchases.getOfferings();
24
+ setOffering(offerings.current);
25
+ } catch (e) {
26
+ console.error('Error fetching offerings:', e);
27
+ Alert.alert(
28
+ translate('common.error'),
29
+ translate('paywall.errors.load_offerings'),
30
+ );
31
+ } finally {
32
+ setIsLoading(false);
33
+ }
34
+ };
35
+ loadOfferings();
36
+ }, []);
37
+
38
+ const handleRestoreCompleted = ({
39
+ customerInfo,
40
+ }: {
41
+ customerInfo: CustomerInfo;
42
+ }) => {
43
+ console.log('Restore completed:', customerInfo);
44
+ };
45
+
46
+ if (isLoading) {
47
+ return (
48
+ <>
49
+ <FocusAwareStatusBar />
50
+ <View
51
+ className="flex-1 items-center justify-center"
52
+ style={{ backgroundColor: theme.colors.background }}
53
+ >
54
+ {/* Could add a loading spinner here */}
55
+ </View>
56
+ </>
57
+ );
58
+ }
59
+
60
+ return (
61
+ <>
62
+ <FocusAwareStatusBar />
63
+ <View
64
+ className="flex-1"
65
+ style={{ backgroundColor: theme.colors.background }}
66
+ >
67
+ <RevenueCatUI.Paywall
68
+ options={{
69
+ offering: offering || undefined,
70
+ }}
71
+ onRestoreCompleted={handleRestoreCompleted}
72
+ onDismiss={() => {
73
+ // Handle dismiss - could navigate back or show completion
74
+ }}
75
+ />
76
+ </View>
77
+ </>
78
+ );
79
+ }
@@ -0,0 +1,95 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ import * as Logger from '@/core/logging';
4
+ import { useAuthClientStore } from '@/lib/state';
5
+
6
+ import { RevenueCatAdapter } from '../services/revenuecat-adapter';
7
+
8
+ /**
9
+ * PaymentInitializer component that initializes RevenueCat SDK on app startup
10
+ * and manages user authentication state with RevenueCat.
11
+ * This component should be rendered at the root level of the app.
12
+ */
13
+ export const PaymentInitializer = () => {
14
+ const { currentUser, isAuthenticated } = useAuthClientStore();
15
+ const previousAuthState = useRef<boolean | null>(null);
16
+ const previousUserId = useRef<string | null>(null);
17
+
18
+ // Initialize RevenueCat SDK on app startup
19
+ useEffect(() => {
20
+ const initializePayments = async () => {
21
+ try {
22
+ await RevenueCatAdapter.initialize();
23
+ Logger.info(
24
+ '[PaymentInitializer] RevenueCat SDK initialized successfully',
25
+ );
26
+ } catch (error) {
27
+ Logger.error(
28
+ '[PaymentInitializer] Failed to initialize RevenueCat SDK: ' +
29
+ String(error),
30
+ );
31
+ // Don't throw error here to prevent app crash
32
+ // The adapter will handle individual method calls gracefully
33
+ }
34
+ };
35
+
36
+ initializePayments();
37
+ }, []);
38
+
39
+ // Handle user authentication state changes (with Fast Refresh protection)
40
+ useEffect(() => {
41
+ const handleAuthStateChange = async () => {
42
+ if (!RevenueCatAdapter.getIsInitialized()) {
43
+ return; // SDK not ready yet
44
+ }
45
+
46
+ const currentUserId = currentUser?.id || null;
47
+
48
+ // Skip if this is the first run (initialization)
49
+ if (previousAuthState.current === null) {
50
+ previousAuthState.current = isAuthenticated;
51
+ previousUserId.current = currentUserId;
52
+ return;
53
+ }
54
+
55
+ // Only react to actual auth state changes, not Fast Refresh temporary states
56
+ const authStateChanged = previousAuthState.current !== isAuthenticated;
57
+ const userIdChanged = previousUserId.current !== currentUserId;
58
+
59
+ if (!authStateChanged && !userIdChanged) {
60
+ return; // No actual change
61
+ }
62
+
63
+ try {
64
+ if (isAuthenticated && currentUserId) {
65
+ // User logged in or switched users - set user ID in RevenueCat
66
+ await RevenueCatAdapter.setUserId(currentUserId);
67
+ Logger.info(
68
+ `[PaymentInitializer] User logged in to RevenueCat: ${currentUserId}`,
69
+ );
70
+ } else if (!isAuthenticated && previousAuthState.current === true) {
71
+ // TEMPORARILY DISABLED: Prevent RevenueCat logout during Fast Refresh
72
+ // This was causing auth session invalidation during development
73
+ // await RevenueCatAdapter.logOut();
74
+ Logger.info(
75
+ '[PaymentInitializer] Skipping RevenueCat logout to prevent Fast Refresh issues',
76
+ );
77
+ }
78
+
79
+ // Update previous state
80
+ previousAuthState.current = isAuthenticated;
81
+ previousUserId.current = currentUserId;
82
+ } catch (error) {
83
+ Logger.error(
84
+ '[PaymentInitializer] Failed to handle auth state change: ' +
85
+ String(error),
86
+ );
87
+ }
88
+ };
89
+
90
+ handleAuthStateChange();
91
+ }, [isAuthenticated, currentUser?.id]);
92
+
93
+ // This component doesn't render anything
94
+ return null;
95
+ };
@@ -0,0 +1,60 @@
1
+ import type React from 'react';
2
+ import { Text, View } from 'react-native';
3
+
4
+ import { Button } from '@/components/ui';
5
+ import { translate } from '@/lib';
6
+ import { useThemeConfig } from '@/lib/use-theme-config';
7
+
8
+ type PaywallErrorStateProps = {
9
+ error: string;
10
+ onRetry: () => void;
11
+ onRestorePurchases: () => void;
12
+ isRestoringPurchases: boolean;
13
+ };
14
+
15
+ export const PaywallErrorState: React.FC<PaywallErrorStateProps> = ({
16
+ error,
17
+ onRetry,
18
+ onRestorePurchases,
19
+ isRestoringPurchases,
20
+ }) => {
21
+ const theme = useThemeConfig();
22
+
23
+ return (
24
+ <>
25
+ {/* Error Message */}
26
+ <View className="mx-4 mb-6">
27
+ <View
28
+ className="rounded-2xl p-6"
29
+ style={{ backgroundColor: `${theme.colors.destructive}10` }}
30
+ >
31
+ <Text
32
+ className="mb-4 text-center font-semibold"
33
+ style={{ color: theme.colors.destructive }}
34
+ >
35
+ {error || translate('paywall.errors.loading_title')}
36
+ </Text>
37
+ <Button
38
+ label={translate('paywall.retry')}
39
+ onPress={onRetry}
40
+ variant="outline"
41
+ />
42
+ </View>
43
+ </View>
44
+
45
+ {/* Restore Purchases */}
46
+ <View className="mx-4 mb-6">
47
+ <Button
48
+ label={
49
+ isRestoringPurchases
50
+ ? translate('paywall.processing')
51
+ : translate('paywall.restore_purchases')
52
+ }
53
+ onPress={onRestorePurchases}
54
+ disabled={isRestoringPurchases}
55
+ variant="outline"
56
+ />
57
+ </View>
58
+ </>
59
+ );
60
+ };