vibefast-cli 0.5.0 → 0.5.2
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/FEATURE-DEPENDENCY-SPEC.md +338 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +219 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/recipes.test.d.ts +2 -0
- package/dist/__tests__/recipes.test.d.ts.map +1 -0
- package/dist/__tests__/recipes.test.js +143 -0
- package/dist/__tests__/recipes.test.js.map +1 -0
- package/dist/commands/__tests__/init.test.d.ts +2 -0
- package/dist/commands/__tests__/init.test.d.ts.map +1 -0
- package/dist/commands/__tests__/init.test.js +95 -0
- package/dist/commands/__tests__/init.test.js.map +1 -0
- package/dist/commands/__tests__/platform.test.d.ts +2 -0
- package/dist/commands/__tests__/platform.test.d.ts.map +1 -0
- package/dist/commands/__tests__/platform.test.js +123 -0
- package/dist/commands/__tests__/platform.test.js.map +1 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +8 -5
- package/dist/commands/add.js.map +1 -1
- package/dist/core/journal.d.ts.map +1 -1
- package/dist/core/journal.js +36 -19
- package/dist/core/journal.js.map +1 -1
- package/dist/core/recipes.d.ts +1 -1
- package/dist/core/recipes.d.ts.map +1 -1
- package/dist/core/recipes.js +12 -41
- package/dist/core/recipes.js.map +1 -1
- package/package.json +1 -1
- package/recipes/ios-widget/recipe.json +78 -0
- package/recipes/ios-widget/targets/widget/AppIntent.swift +46 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/CalorieTrackerWidget.swift +424 -0
- package/recipes/ios-widget/targets/widget/HabitTrackerWidget.swift +305 -0
- package/recipes/ios-widget/targets/widget/Info.plist +11 -0
- package/recipes/ios-widget/targets/widget/WidgetLiveActivity.swift +75 -0
- package/recipes/ios-widget/targets/widget/expo-target.config.js +10 -0
- package/recipes/ios-widget/targets/widget/generated.entitlements +5 -0
- package/recipes/ios-widget/targets/widget/index.swift +18 -0
- package/recipes/ios-widget/targets/widget/widgets.swift +96 -0
- package/recipes/ios-widget@latest.zip +0 -0
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +74 -0
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +25 -0
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +23 -0
- package/recipes/payments/apps/native/src/features/payments/README.md +200 -0
- package/recipes/payments/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
- package/recipes/payments/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
- package/recipes/payments/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
- package/recipes/payments/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
- package/recipes/payments/apps/native/src/features/payments/index.ts +8 -0
- package/recipes/payments/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
- package/recipes/payments/recipe.json +58 -0
- package/recipes/payments@latest.zip +0 -0
- package/src/__tests__/integration.test.ts +249 -0
- package/src/__tests__/recipes.test.ts +168 -0
- package/src/commands/__tests__/init.test.ts +112 -0
- package/src/commands/__tests__/platform.test.ts +141 -0
- package/src/commands/add.ts +9 -5
- package/src/core/journal.ts +42 -25
- package/src/core/recipes.ts +12 -42
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { ActivityIndicator, ScrollView, Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { ProductOffering } from '@/core/payments/payment-service';
|
|
5
|
+
import { translate } from '@/lib';
|
|
6
|
+
import { useThemeConfig } from '@/lib/use-theme-config';
|
|
7
|
+
|
|
8
|
+
import { PaywallErrorState } from './paywall-error-state';
|
|
9
|
+
import { PaywallProductCard } from './paywall-product-card';
|
|
10
|
+
|
|
11
|
+
type PaywallLocalModeProps = {
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
offerings: ProductOffering[];
|
|
15
|
+
onPurchase: (productId: string) => void;
|
|
16
|
+
onRetry: () => void;
|
|
17
|
+
onRestorePurchases: () => void;
|
|
18
|
+
isPurchasing: boolean;
|
|
19
|
+
primaryColor: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const PaywallLocalMode: React.FC<PaywallLocalModeProps> = ({
|
|
23
|
+
isLoading,
|
|
24
|
+
error,
|
|
25
|
+
offerings,
|
|
26
|
+
onPurchase,
|
|
27
|
+
onRetry,
|
|
28
|
+
onRestorePurchases,
|
|
29
|
+
isPurchasing,
|
|
30
|
+
primaryColor,
|
|
31
|
+
}) => {
|
|
32
|
+
const theme = useThemeConfig();
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View className="flex-1">
|
|
36
|
+
<View className="mb-8 items-center px-4">
|
|
37
|
+
<View
|
|
38
|
+
className="mb-6 size-32 items-center justify-center rounded-full"
|
|
39
|
+
style={{
|
|
40
|
+
backgroundColor: `${theme.colors.primary}10`,
|
|
41
|
+
borderWidth: 3,
|
|
42
|
+
borderColor: theme.colors.primary,
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<Text className="text-6xl">✨</Text>
|
|
46
|
+
</View>
|
|
47
|
+
<Text
|
|
48
|
+
className="mb-4 text-center text-3xl font-bold"
|
|
49
|
+
style={{ color: theme.colors.foreground }}
|
|
50
|
+
>
|
|
51
|
+
{translate('paywall.title')}
|
|
52
|
+
</Text>
|
|
53
|
+
<Text
|
|
54
|
+
className="text-center text-lg leading-6"
|
|
55
|
+
style={{ color: theme.colors.mutedForeground }}
|
|
56
|
+
>
|
|
57
|
+
{translate('paywall.subtitle')}
|
|
58
|
+
</Text>
|
|
59
|
+
</View>
|
|
60
|
+
|
|
61
|
+
{isLoading && (
|
|
62
|
+
<View className="flex-1 items-center justify-center py-20">
|
|
63
|
+
<ActivityIndicator size="large" color={theme.colors.primary} />
|
|
64
|
+
<Text
|
|
65
|
+
className="mt-4 text-lg"
|
|
66
|
+
style={{ color: theme.colors.mutedForeground }}
|
|
67
|
+
>
|
|
68
|
+
{translate('paywall.loading')}
|
|
69
|
+
</Text>
|
|
70
|
+
</View>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{error && !isLoading && (
|
|
74
|
+
<PaywallErrorState
|
|
75
|
+
error={error}
|
|
76
|
+
onRetry={onRetry}
|
|
77
|
+
onRestorePurchases={onRestorePurchases}
|
|
78
|
+
isRestoringPurchases={isPurchasing}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
{!isLoading && !error && offerings.length === 0 && (
|
|
83
|
+
<View className="mx-4 mb-6">
|
|
84
|
+
<View
|
|
85
|
+
className="rounded-2xl p-6"
|
|
86
|
+
style={{ backgroundColor: `${theme.colors.warning}10` }}
|
|
87
|
+
>
|
|
88
|
+
<Text
|
|
89
|
+
className="text-center"
|
|
90
|
+
style={{ color: theme.colors.warning }}
|
|
91
|
+
>
|
|
92
|
+
{translate('paywall.no_offerings')}
|
|
93
|
+
</Text>
|
|
94
|
+
</View>
|
|
95
|
+
</View>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{!isLoading && !error && offerings.length > 0 && (
|
|
99
|
+
<ScrollView
|
|
100
|
+
showsVerticalScrollIndicator={false}
|
|
101
|
+
contentContainerStyle={{ paddingBottom: 20 }}
|
|
102
|
+
>
|
|
103
|
+
{offerings.map((offering) => (
|
|
104
|
+
<PaywallProductCard
|
|
105
|
+
key={offering.id}
|
|
106
|
+
offering={offering}
|
|
107
|
+
onPurchase={onPurchase}
|
|
108
|
+
isLoading={isPurchasing}
|
|
109
|
+
primaryColor={primaryColor}
|
|
110
|
+
/>
|
|
111
|
+
))}
|
|
112
|
+
</ScrollView>
|
|
113
|
+
)}
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
};
|
package/recipes/payments/apps/native/src/features/payments/components/paywall-product-card.tsx
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { ActivityIndicator, Pressable, Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { ProductOffering } from '@/core/payments/payment-service';
|
|
5
|
+
import { translate } from '@/lib';
|
|
6
|
+
import { useThemeConfig } from '@/lib/use-theme-config';
|
|
7
|
+
|
|
8
|
+
type PaywallProductCardProps = {
|
|
9
|
+
offering: ProductOffering;
|
|
10
|
+
onPurchase: (productId: string) => void;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
primaryColor: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const PaywallProductCard: React.FC<PaywallProductCardProps> = ({
|
|
16
|
+
offering,
|
|
17
|
+
onPurchase,
|
|
18
|
+
isLoading,
|
|
19
|
+
primaryColor,
|
|
20
|
+
}) => {
|
|
21
|
+
const theme = useThemeConfig();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View
|
|
25
|
+
key={offering.id}
|
|
26
|
+
className="mx-4 mb-6 overflow-hidden rounded-3xl"
|
|
27
|
+
style={{
|
|
28
|
+
backgroundColor: theme.colors.card,
|
|
29
|
+
shadowColor: theme.colors.primary,
|
|
30
|
+
shadowOffset: { width: 0, height: 20 },
|
|
31
|
+
shadowOpacity: 0.15,
|
|
32
|
+
shadowRadius: 40,
|
|
33
|
+
elevation: 8,
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{/* Popular Badge */}
|
|
37
|
+
<View
|
|
38
|
+
className="absolute right-6 top-0 z-10 rounded-b-2xl px-4 py-2"
|
|
39
|
+
style={{ backgroundColor: theme.colors.primary }}
|
|
40
|
+
>
|
|
41
|
+
<Text
|
|
42
|
+
className="text-xs font-bold"
|
|
43
|
+
style={{ color: theme.colors.primaryForeground }}
|
|
44
|
+
>
|
|
45
|
+
{translate('paywall.product_card.badge')}
|
|
46
|
+
</Text>
|
|
47
|
+
</View>
|
|
48
|
+
|
|
49
|
+
<View className="p-6 pt-8">
|
|
50
|
+
{/* Header */}
|
|
51
|
+
<View className="mb-6 items-center">
|
|
52
|
+
<View
|
|
53
|
+
className="mb-4 size-20 items-center justify-center rounded-full"
|
|
54
|
+
style={{
|
|
55
|
+
backgroundColor: `${primaryColor}20`,
|
|
56
|
+
borderWidth: 3,
|
|
57
|
+
borderColor: `${primaryColor}30`,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<Text className="text-3xl">💎</Text>
|
|
61
|
+
</View>
|
|
62
|
+
|
|
63
|
+
<Text
|
|
64
|
+
className="mb-2 text-xl font-bold"
|
|
65
|
+
style={{ color: theme.colors.foreground }}
|
|
66
|
+
>
|
|
67
|
+
{translate('paywall.product_card.title')}
|
|
68
|
+
</Text>
|
|
69
|
+
|
|
70
|
+
<Text className="text-3xl font-black" style={{ color: primaryColor }}>
|
|
71
|
+
{offering.priceString}
|
|
72
|
+
</Text>
|
|
73
|
+
</View>
|
|
74
|
+
|
|
75
|
+
{/* Features */}
|
|
76
|
+
<View className="mb-6 space-y-3">
|
|
77
|
+
<View className="flex-row items-center">
|
|
78
|
+
<Text className="mr-2 text-base">✨</Text>
|
|
79
|
+
<Text
|
|
80
|
+
className="text-base"
|
|
81
|
+
style={{ color: theme.colors.mutedForeground }}
|
|
82
|
+
>
|
|
83
|
+
{translate('paywall.product_card.features.ai_generations')}
|
|
84
|
+
</Text>
|
|
85
|
+
</View>
|
|
86
|
+
<View className="flex-row items-center">
|
|
87
|
+
<Text className="mr-2 text-base">🚀</Text>
|
|
88
|
+
<Text
|
|
89
|
+
className="text-base"
|
|
90
|
+
style={{ color: theme.colors.mutedForeground }}
|
|
91
|
+
>
|
|
92
|
+
{translate('paywall.product_card.features.priority_processing')}
|
|
93
|
+
</Text>
|
|
94
|
+
</View>
|
|
95
|
+
<View className="flex-row items-center">
|
|
96
|
+
<Text className="mr-2 text-base">💫</Text>
|
|
97
|
+
<Text
|
|
98
|
+
className="text-base"
|
|
99
|
+
style={{ color: theme.colors.mutedForeground }}
|
|
100
|
+
>
|
|
101
|
+
{translate('paywall.product_card.features.advanced_models')}
|
|
102
|
+
</Text>
|
|
103
|
+
</View>
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
{/* Purchase Button */}
|
|
107
|
+
<Pressable
|
|
108
|
+
onPress={() => onPurchase(offering.id)}
|
|
109
|
+
disabled={isLoading}
|
|
110
|
+
className="items-center justify-center rounded-2xl py-4"
|
|
111
|
+
style={{
|
|
112
|
+
backgroundColor: primaryColor,
|
|
113
|
+
opacity: isLoading ? 0.7 : 1,
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{isLoading ? (
|
|
117
|
+
<ActivityIndicator
|
|
118
|
+
size="small"
|
|
119
|
+
color={theme.colors.primaryForeground}
|
|
120
|
+
/>
|
|
121
|
+
) : (
|
|
122
|
+
<Text
|
|
123
|
+
className="text-lg font-bold"
|
|
124
|
+
style={{ color: theme.colors.primaryForeground }}
|
|
125
|
+
>
|
|
126
|
+
{translate('paywall.product_card.cta')}
|
|
127
|
+
</Text>
|
|
128
|
+
)}
|
|
129
|
+
</Pressable>
|
|
130
|
+
</View>
|
|
131
|
+
</View>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { ActivityIndicator, Pressable, Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { translate } from '@/lib';
|
|
5
|
+
import { useThemeConfig } from '@/lib/use-theme-config';
|
|
6
|
+
|
|
7
|
+
type PaywallRemoteModeProps = {
|
|
8
|
+
onPresentPaywall: () => void;
|
|
9
|
+
isPurchasing: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const PaywallRemoteMode: React.FC<PaywallRemoteModeProps> = ({
|
|
13
|
+
onPresentPaywall,
|
|
14
|
+
isPurchasing,
|
|
15
|
+
}) => {
|
|
16
|
+
const theme = useThemeConfig();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View className="flex-1 px-4">
|
|
20
|
+
<View className="mb-8 items-center">
|
|
21
|
+
<View
|
|
22
|
+
className="mb-6 size-32 items-center justify-center rounded-full"
|
|
23
|
+
style={{
|
|
24
|
+
backgroundColor: `${theme.colors.primary}10`,
|
|
25
|
+
borderWidth: 3,
|
|
26
|
+
borderColor: theme.colors.primary,
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<Text className="text-6xl">🎨</Text>
|
|
30
|
+
</View>
|
|
31
|
+
<Text
|
|
32
|
+
className="mb-4 text-center text-3xl font-bold"
|
|
33
|
+
style={{ color: theme.colors.foreground }}
|
|
34
|
+
>
|
|
35
|
+
{translate('paywall.remote.hero_title')}
|
|
36
|
+
</Text>
|
|
37
|
+
<Text
|
|
38
|
+
className="mb-8 text-center text-lg leading-6"
|
|
39
|
+
style={{ color: theme.colors.mutedForeground }}
|
|
40
|
+
>
|
|
41
|
+
{translate('paywall.remote.hero_subtitle')}
|
|
42
|
+
</Text>
|
|
43
|
+
</View>
|
|
44
|
+
|
|
45
|
+
<View
|
|
46
|
+
className="mb-6 rounded-3xl p-8"
|
|
47
|
+
style={{
|
|
48
|
+
backgroundColor: theme.colors.card,
|
|
49
|
+
shadowColor: theme.colors.primary,
|
|
50
|
+
shadowOffset: { width: 0, height: 20 },
|
|
51
|
+
shadowOpacity: 0.1,
|
|
52
|
+
shadowRadius: 30,
|
|
53
|
+
elevation: 8,
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<View className="mb-6 items-center">
|
|
57
|
+
<Text
|
|
58
|
+
className="mb-2 text-xl font-semibold"
|
|
59
|
+
style={{ color: theme.colors.foreground }}
|
|
60
|
+
>
|
|
61
|
+
{translate('paywall.remote.features_title')}
|
|
62
|
+
</Text>
|
|
63
|
+
<View
|
|
64
|
+
className="h-1 w-16 rounded-full"
|
|
65
|
+
style={{ backgroundColor: theme.colors.primary }}
|
|
66
|
+
/>
|
|
67
|
+
</View>
|
|
68
|
+
|
|
69
|
+
<View className="gap-y-4">
|
|
70
|
+
<View className="flex-row items-center">
|
|
71
|
+
<View
|
|
72
|
+
className="mr-3 size-8 items-center justify-center rounded-full"
|
|
73
|
+
style={{ backgroundColor: `${theme.colors.success}20` }}
|
|
74
|
+
>
|
|
75
|
+
<Text className="text-sm">🎨</Text>
|
|
76
|
+
</View>
|
|
77
|
+
<Text style={{ color: theme.colors.mutedForeground }}>
|
|
78
|
+
{translate('paywall.remote.features.native_ui')}
|
|
79
|
+
</Text>
|
|
80
|
+
</View>
|
|
81
|
+
<View className="flex-row items-center">
|
|
82
|
+
<View
|
|
83
|
+
className="mr-3 size-8 items-center justify-center rounded-full"
|
|
84
|
+
style={{ backgroundColor: `${theme.colors.success}20` }}
|
|
85
|
+
>
|
|
86
|
+
<Text className="text-sm">🔄</Text>
|
|
87
|
+
</View>
|
|
88
|
+
<Text style={{ color: theme.colors.mutedForeground }}>
|
|
89
|
+
{translate('paywall.remote.features.realtime_updates')}
|
|
90
|
+
</Text>
|
|
91
|
+
</View>
|
|
92
|
+
<View className="flex-row items-center">
|
|
93
|
+
<View
|
|
94
|
+
className="mr-3 size-8 items-center justify-center rounded-full"
|
|
95
|
+
style={{ backgroundColor: `${theme.colors.success}20` }}
|
|
96
|
+
>
|
|
97
|
+
<Text className="text-sm">📱</Text>
|
|
98
|
+
</View>
|
|
99
|
+
<Text style={{ color: theme.colors.mutedForeground }}>
|
|
100
|
+
{translate('paywall.remote.features.mobile_optimized')}
|
|
101
|
+
</Text>
|
|
102
|
+
</View>
|
|
103
|
+
</View>
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
<Pressable
|
|
107
|
+
onPress={onPresentPaywall}
|
|
108
|
+
disabled={isPurchasing}
|
|
109
|
+
className="mb-4 items-center justify-center rounded-3xl py-6"
|
|
110
|
+
style={{
|
|
111
|
+
backgroundColor: isPurchasing
|
|
112
|
+
? theme.colors.muted
|
|
113
|
+
: theme.colors.primary,
|
|
114
|
+
opacity: isPurchasing ? 0.6 : 1,
|
|
115
|
+
shadowColor: theme.colors.primary,
|
|
116
|
+
shadowOffset: { width: 0, height: 8 },
|
|
117
|
+
shadowOpacity: 0.3,
|
|
118
|
+
shadowRadius: 16,
|
|
119
|
+
elevation: 8,
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{isPurchasing ? (
|
|
123
|
+
<View className="flex-row items-center">
|
|
124
|
+
<ActivityIndicator
|
|
125
|
+
size="small"
|
|
126
|
+
color={theme.colors.primaryForeground}
|
|
127
|
+
/>
|
|
128
|
+
<Text
|
|
129
|
+
className="ml-3 text-lg font-bold"
|
|
130
|
+
style={{ color: theme.colors.primaryForeground }}
|
|
131
|
+
>
|
|
132
|
+
{translate('common.loading')}
|
|
133
|
+
</Text>
|
|
134
|
+
</View>
|
|
135
|
+
) : (
|
|
136
|
+
<Text
|
|
137
|
+
className="text-xl font-bold"
|
|
138
|
+
style={{ color: theme.colors.primaryForeground }}
|
|
139
|
+
>
|
|
140
|
+
{translate('paywall.remote.cta')}
|
|
141
|
+
</Text>
|
|
142
|
+
)}
|
|
143
|
+
</Pressable>
|
|
144
|
+
</View>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { RevenueCatAdapter } from '@/features/payments/services/revenuecat-adapter';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to check if a user has a specific entitlement
|
|
7
|
+
* @param entitlementId - The entitlement identifier to check
|
|
8
|
+
* @returns Object containing entitlement status, loading state, and any error
|
|
9
|
+
*/
|
|
10
|
+
export const useEntitlement = (entitlementId: string) => {
|
|
11
|
+
const [isEntitled, setIsEntitled] = useState(false);
|
|
12
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let isMounted = true;
|
|
17
|
+
|
|
18
|
+
const checkEntitlement = async () => {
|
|
19
|
+
if (!entitlementId) {
|
|
20
|
+
if (!isMounted) return;
|
|
21
|
+
setIsEntitled(false);
|
|
22
|
+
setIsLoading(false);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
if (!isMounted) return;
|
|
28
|
+
setIsLoading(true);
|
|
29
|
+
setError(null);
|
|
30
|
+
|
|
31
|
+
if (!RevenueCatAdapter.getIsInitialized()) {
|
|
32
|
+
await RevenueCatAdapter.initialize();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const paymentService = new RevenueCatAdapter();
|
|
36
|
+
const { isActive } =
|
|
37
|
+
await paymentService.getUserSubscriptionStatus(entitlementId);
|
|
38
|
+
if (!isMounted) return;
|
|
39
|
+
setIsEntitled(isActive);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
if (!isMounted) return;
|
|
42
|
+
setIsEntitled(false); // Default to not entitled on error
|
|
43
|
+
setError(
|
|
44
|
+
e instanceof Error ? e.message : 'Failed to check entitlement',
|
|
45
|
+
);
|
|
46
|
+
} finally {
|
|
47
|
+
if (!isMounted) return;
|
|
48
|
+
setIsLoading(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
checkEntitlement();
|
|
53
|
+
|
|
54
|
+
// Optional: Add listener for customer info updates from RevenueCat
|
|
55
|
+
// This would require implementing a listener mechanism in RevenueCatAdapter
|
|
56
|
+
// For now, we'll rely on component re-mounting or manual refresh
|
|
57
|
+
return () => {
|
|
58
|
+
isMounted = false;
|
|
59
|
+
};
|
|
60
|
+
}, [entitlementId]);
|
|
61
|
+
|
|
62
|
+
return { isEntitled, isLoading, error };
|
|
63
|
+
};
|