create-stackr 0.2.0
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/LICENSE +21 -0
- package/README.md +642 -0
- package/bin/cli.js +12 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/dependencies.d.ts +82 -0
- package/dist/config/dependencies.d.ts.map +1 -0
- package/dist/config/dependencies.js +82 -0
- package/dist/config/dependencies.js.map +1 -0
- package/dist/config/presets.d.ts +3 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/presets.js +174 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/generators/index.d.ts +40 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +130 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/onboarding.d.ts +8 -0
- package/dist/generators/onboarding.d.ts.map +1 -0
- package/dist/generators/onboarding.js +141 -0
- package/dist/generators/onboarding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/features.d.ts +14 -0
- package/dist/prompts/features.d.ts.map +1 -0
- package/dist/prompts/features.js +96 -0
- package/dist/prompts/features.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +93 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/onboarding.d.ts +6 -0
- package/dist/prompts/onboarding.d.ts.map +1 -0
- package/dist/prompts/onboarding.js +37 -0
- package/dist/prompts/onboarding.js.map +1 -0
- package/dist/prompts/orm.d.ts +3 -0
- package/dist/prompts/orm.d.ts.map +1 -0
- package/dist/prompts/orm.js +23 -0
- package/dist/prompts/orm.js.map +1 -0
- package/dist/prompts/packageManager.d.ts +2 -0
- package/dist/prompts/packageManager.d.ts.map +1 -0
- package/dist/prompts/packageManager.js +18 -0
- package/dist/prompts/packageManager.js.map +1 -0
- package/dist/prompts/platform.d.ts +3 -0
- package/dist/prompts/platform.d.ts.map +1 -0
- package/dist/prompts/platform.js +21 -0
- package/dist/prompts/platform.js.map +1 -0
- package/dist/prompts/preset.d.ts +4 -0
- package/dist/prompts/preset.d.ts.map +1 -0
- package/dist/prompts/preset.js +165 -0
- package/dist/prompts/preset.js.map +1 -0
- package/dist/prompts/project.d.ts +2 -0
- package/dist/prompts/project.d.ts.map +1 -0
- package/dist/prompts/project.js +27 -0
- package/dist/prompts/project.js.map +1 -0
- package/dist/prompts/sdks.d.ts +2 -0
- package/dist/prompts/sdks.d.ts.map +1 -0
- package/dist/prompts/sdks.js +46 -0
- package/dist/prompts/sdks.js.map +1 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cleanup.d.ts +5 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +38 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/copy.d.ts +10 -0
- package/dist/utils/copy.d.ts.map +1 -0
- package/dist/utils/copy.js +53 -0
- package/dist/utils/copy.js.map +1 -0
- package/dist/utils/errors.d.ts +33 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +136 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +33 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package.d.ts +16 -0
- package/dist/utils/package.d.ts.map +1 -0
- package/dist/utils/package.js +86 -0
- package/dist/utils/package.js.map +1 -0
- package/dist/utils/system-validation.d.ts +9 -0
- package/dist/utils/system-validation.d.ts.map +1 -0
- package/dist/utils/system-validation.js +31 -0
- package/dist/utils/system-validation.js.map +1 -0
- package/dist/utils/template.d.ts +20 -0
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +234 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +94 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +96 -0
- package/templates/base/backend/.dockerignore.ejs +62 -0
- package/templates/base/backend/.env.example.ejs +116 -0
- package/templates/base/backend/Dockerfile.ejs +142 -0
- package/templates/base/backend/controllers/event-queue/index.ts +20 -0
- package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
- package/templates/base/backend/controllers/rest-api/index.ts +48 -0
- package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
- package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
- package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
- package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
- package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
- package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
- package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
- package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
- package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
- package/templates/base/backend/domain/device-session/schema.ts +72 -0
- package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
- package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
- package/templates/base/backend/domain/session/schema.ts +29 -0
- package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
- package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
- package/templates/base/backend/domain/user/schema.ts +14 -0
- package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
- package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
- package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
- package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
- package/templates/base/backend/lib/constants.ts.ejs +29 -0
- package/templates/base/backend/package.json.ejs +50 -0
- package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
- package/templates/base/backend/prisma.config.prisma.ts +12 -0
- package/templates/base/backend/tsconfig.json +39 -0
- package/templates/base/backend/utils/db.drizzle.ts +41 -0
- package/templates/base/backend/utils/db.prisma.ts +51 -0
- package/templates/base/backend/utils/email.ts.ejs +35 -0
- package/templates/base/backend/utils/errors.ts +348 -0
- package/templates/base/backend/utils/redis.ts.ejs +279 -0
- package/templates/base/mobile/.env.example.ejs +35 -0
- package/templates/base/mobile/.gitignore.ejs +167 -0
- package/templates/base/mobile/app/+not-found.tsx +85 -0
- package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
- package/templates/base/mobile/app.json.ejs +88 -0
- package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
- package/templates/base/mobile/assets/images/favicon.png +0 -0
- package/templates/base/mobile/assets/images/icon.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
- package/templates/base/mobile/assets/images/paywall_image.png +0 -0
- package/templates/base/mobile/assets/images/splash.png +0 -0
- package/templates/base/mobile/eas.json.ejs +49 -0
- package/templates/base/mobile/metro.config.js +9 -0
- package/templates/base/mobile/package.json.ejs +53 -0
- package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
- package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
- package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
- package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
- package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
- package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
- package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
- package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
- package/templates/base/mobile/src/components/ui/index.ts +6 -0
- package/templates/base/mobile/src/constants/Theme.ts +163 -0
- package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
- package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
- package/templates/base/mobile/src/services/api.ts.ejs +71 -0
- package/templates/base/mobile/src/services/errorService.ts +179 -0
- package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
- package/templates/base/mobile/src/store/index.ts.ejs +18 -0
- package/templates/base/mobile/src/store/ui.store.ts +100 -0
- package/templates/base/mobile/src/utils/formatters.ts +105 -0
- package/templates/base/mobile/src/utils/logger.ts +73 -0
- package/templates/base/mobile/src/utils/responsive.ts +234 -0
- package/templates/base/mobile/tsconfig.json +32 -0
- package/templates/base/web/.env.example.ejs +26 -0
- package/templates/base/web/components.json +22 -0
- package/templates/base/web/eslint.config.mjs +18 -0
- package/templates/base/web/next.config.ts +7 -0
- package/templates/base/web/package.json.ejs +35 -0
- package/templates/base/web/postcss.config.mjs +7 -0
- package/templates/base/web/public/.gitkeep +0 -0
- package/templates/base/web/public/file.svg +1 -0
- package/templates/base/web/public/globe.svg +1 -0
- package/templates/base/web/public/next.svg +1 -0
- package/templates/base/web/public/vercel.svg +1 -0
- package/templates/base/web/public/window.svg +1 -0
- package/templates/base/web/src/app/favicon.ico +0 -0
- package/templates/base/web/src/app/globals.css +152 -0
- package/templates/base/web/src/app/layout.tsx.ejs +54 -0
- package/templates/base/web/src/app/page.tsx.ejs +92 -0
- package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
- package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
- package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
- package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
- package/templates/base/web/src/components/theme-toggle.tsx +34 -0
- package/templates/base/web/src/components/ui/button.tsx +62 -0
- package/templates/base/web/src/components/ui/card.tsx +92 -0
- package/templates/base/web/src/components/ui/input.tsx +21 -0
- package/templates/base/web/src/components/ui/label.tsx +24 -0
- package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/web/src/components/ui/spinner.tsx +20 -0
- package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
- package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
- package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
- package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
- package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
- package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
- package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
- package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
- package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
- package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
- package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
- package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
- package/templates/base/web/src/lib/utils.ts +6 -0
- package/templates/base/web/src/proxy.ts.ejs +66 -0
- package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
- package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
- package/templates/base/web/tsconfig.json +34 -0
- package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
- package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
- package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
- package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
- package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
- package/templates/features/mobile/auth/components/auth/index.ts +2 -0
- package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
- package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
- package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
- package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
- package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
- package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
- package/templates/features/web/.gitkeep +0 -0
- package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
- package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
- package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
- package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
- package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
- package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
- package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
- package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
- package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
- package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
- package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
- package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
- package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
- package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
- package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
- package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
- package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
- package/templates/integrations/mobile/att/services/attService.ts +84 -0
- package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
- package/templates/integrations/mobile/att/store/att.store.ts +162 -0
- package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
- package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
- package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
- package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
- package/templates/integrations/web/.gitkeep +0 -0
- package/templates/shared/.env.example.ejs +21 -0
- package/templates/shared/.gitignore.ejs +145 -0
- package/templates/shared/README.md.ejs +134 -0
- package/templates/shared/docker-compose.prod.yml.ejs +120 -0
- package/templates/shared/docker-compose.yml.ejs +129 -0
- package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
- package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
- package/templates/shared/scripts/setup.sh.ejs +979 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Image, StyleSheet } from 'react-native';
|
|
3
|
+
import { router } from 'expo-router';
|
|
4
|
+
import OnboardingLayout from '@/components/ui/OnboardingLayout';
|
|
5
|
+
import { responsive } from '@/utils/responsive';
|
|
6
|
+
|
|
7
|
+
export default function OnboardingPage1() {
|
|
8
|
+
const handleContinue = () => {
|
|
9
|
+
<% if (features.onboarding.pages === 1) { %>
|
|
10
|
+
<% if (features.paywall) { %>
|
|
11
|
+
router.replace('/paywall');
|
|
12
|
+
<% } else { %>
|
|
13
|
+
router.replace('/(tabs)');
|
|
14
|
+
<% } %>
|
|
15
|
+
<% } else { %>
|
|
16
|
+
router.replace('/(onboarding)/page-2');
|
|
17
|
+
<% } %>
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<OnboardingLayout
|
|
22
|
+
title="Welcome to Your App"
|
|
23
|
+
subtitle="Get started with powerful features designed to make your life easier."
|
|
24
|
+
pageIndicators={1}
|
|
25
|
+
totalPages={<%= features.onboarding.pages %>}
|
|
26
|
+
onContinue={handleContinue}
|
|
27
|
+
>
|
|
28
|
+
<View style={styles.imageContainer}>
|
|
29
|
+
<Image
|
|
30
|
+
source={require('@/assets/images/onboarding_page_1.png')}
|
|
31
|
+
style={styles.onboardingImage}
|
|
32
|
+
resizeMode="contain"
|
|
33
|
+
/>
|
|
34
|
+
</View>
|
|
35
|
+
</OnboardingLayout>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const imageSize = responsive.getImageContainerSize();
|
|
40
|
+
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
imageContainer: {
|
|
43
|
+
width: imageSize.width,
|
|
44
|
+
height: imageSize.height,
|
|
45
|
+
justifyContent: 'center',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
},
|
|
48
|
+
onboardingImage: {
|
|
49
|
+
width: '100%',
|
|
50
|
+
height: '100%',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Image, StyleSheet } from 'react-native';
|
|
3
|
+
import { router } from 'expo-router';
|
|
4
|
+
import OnboardingLayout from '@/components/ui/OnboardingLayout';
|
|
5
|
+
import { responsive } from '@/utils/responsive';
|
|
6
|
+
|
|
7
|
+
export default function OnboardingPage2() {
|
|
8
|
+
const handleContinue = () => {
|
|
9
|
+
<% if (features.onboarding.pages === 2) { %>
|
|
10
|
+
<% if (features.paywall) { %>
|
|
11
|
+
router.replace('/paywall');
|
|
12
|
+
<% } else { %>
|
|
13
|
+
router.replace('/(tabs)');
|
|
14
|
+
<% } %>
|
|
15
|
+
<% } else { %>
|
|
16
|
+
router.replace('/(onboarding)/page-3');
|
|
17
|
+
<% } %>
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<OnboardingLayout
|
|
22
|
+
title="Powerful Features"
|
|
23
|
+
subtitle="Unlock advanced capabilities and streamlined workflows to boost your productivity."
|
|
24
|
+
pageIndicators={2}
|
|
25
|
+
totalPages={<%= features.onboarding.pages %>}
|
|
26
|
+
onContinue={handleContinue}
|
|
27
|
+
>
|
|
28
|
+
<View style={styles.imageContainer}>
|
|
29
|
+
<Image
|
|
30
|
+
source={require('@/assets/images/onboarding_page_2.png')}
|
|
31
|
+
style={styles.onboardingImage}
|
|
32
|
+
resizeMode="contain"
|
|
33
|
+
/>
|
|
34
|
+
</View>
|
|
35
|
+
</OnboardingLayout>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const imageSize = responsive.getImageContainerSize();
|
|
40
|
+
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
imageContainer: {
|
|
43
|
+
width: imageSize.width,
|
|
44
|
+
height: imageSize.height,
|
|
45
|
+
justifyContent: 'center',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
},
|
|
48
|
+
onboardingImage: {
|
|
49
|
+
width: '100%',
|
|
50
|
+
height: '100%',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Image, StyleSheet } from 'react-native';
|
|
3
|
+
import { router } from 'expo-router';
|
|
4
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
5
|
+
import OnboardingLayout from '@/components/ui/OnboardingLayout';
|
|
6
|
+
import { responsive } from '@/utils/responsive';
|
|
7
|
+
|
|
8
|
+
export default function OnboardingPage3() {
|
|
9
|
+
const handleGetStarted = async () => {
|
|
10
|
+
try {
|
|
11
|
+
await AsyncStorage.setItem('onboarding_completed', 'true');
|
|
12
|
+
<% if (features.paywall) { %>
|
|
13
|
+
router.replace('/paywall');
|
|
14
|
+
<% } else { %>
|
|
15
|
+
router.replace('/(tabs)');
|
|
16
|
+
<% } %>
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Failed to save onboarding completion status:', error);
|
|
19
|
+
<% if (features.paywall) { %>
|
|
20
|
+
router.replace('/paywall');
|
|
21
|
+
<% } else { %>
|
|
22
|
+
router.replace('/(tabs)');
|
|
23
|
+
<% } %>
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<OnboardingLayout
|
|
29
|
+
title="Ready to Get Started"
|
|
30
|
+
subtitle="You're all set! Let's unlock the full potential of your new experience."
|
|
31
|
+
pageIndicators={3}
|
|
32
|
+
totalPages={<%= features.onboarding.pages %>}
|
|
33
|
+
onContinue={handleGetStarted}
|
|
34
|
+
continueText="Get Started"
|
|
35
|
+
>
|
|
36
|
+
<View style={styles.imageContainer}>
|
|
37
|
+
<Image
|
|
38
|
+
source={require('@/assets/images/onboarding_page_3.png')}
|
|
39
|
+
style={styles.onboardingImage}
|
|
40
|
+
resizeMode="contain"
|
|
41
|
+
/>
|
|
42
|
+
</View>
|
|
43
|
+
</OnboardingLayout>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const imageSize = responsive.getImageContainerSize();
|
|
48
|
+
|
|
49
|
+
const styles = StyleSheet.create({
|
|
50
|
+
imageContainer: {
|
|
51
|
+
width: imageSize.width,
|
|
52
|
+
height: imageSize.height,
|
|
53
|
+
justifyContent: 'center',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
},
|
|
56
|
+
onboardingImage: {
|
|
57
|
+
width: '100%',
|
|
58
|
+
height: '100%',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Alert,
|
|
8
|
+
ActivityIndicator,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { IconSymbol } from '../src/components/ui/IconSymbol';
|
|
11
|
+
import { Theme } from '@/constants/Theme';
|
|
12
|
+
import { router, Stack } from 'expo-router';
|
|
13
|
+
import { useRevenueCat, useRevenueCatActions } from '../src/store/revenuecat.store';
|
|
14
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
15
|
+
import { logger } from '../src/utils/logger';
|
|
16
|
+
import OnboardingLayout from '../src/components/ui/OnboardingLayout';
|
|
17
|
+
import { responsive, fontSize, getSpacing } from '@/utils/responsive';
|
|
18
|
+
|
|
19
|
+
interface PlanOption {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
price: string;
|
|
23
|
+
period: string;
|
|
24
|
+
badge?: string;
|
|
25
|
+
revenueCatPackage: any;
|
|
26
|
+
pricePerWeek?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function PaywallScreen() {
|
|
30
|
+
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
|
|
31
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
customerInfo,
|
|
35
|
+
isLoading: subscriptionLoading,
|
|
36
|
+
offerings,
|
|
37
|
+
} = useRevenueCat();
|
|
38
|
+
|
|
39
|
+
const { getOfferings, purchasePackage, restorePurchases } = useRevenueCatActions();
|
|
40
|
+
|
|
41
|
+
const hasActiveSubscription = customerInfo?.entitlements.active ?
|
|
42
|
+
Object.keys(customerInfo.entitlements.active).length > 0 : false;
|
|
43
|
+
|
|
44
|
+
// Fetch offerings when component mounts
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
getOfferings();
|
|
47
|
+
}, [getOfferings]);
|
|
48
|
+
|
|
49
|
+
const hasOfferings = useMemo(() => {
|
|
50
|
+
return offerings && Object.keys(offerings.all).length > 0;
|
|
51
|
+
}, [offerings]);
|
|
52
|
+
|
|
53
|
+
const plans: PlanOption[] = useMemo(() => {
|
|
54
|
+
const mappedPlans: PlanOption[] = [];
|
|
55
|
+
|
|
56
|
+
if (hasOfferings && offerings) {
|
|
57
|
+
// Use real offerings from RevenueCat
|
|
58
|
+
for (const key in offerings.all) {
|
|
59
|
+
if (offerings.all.hasOwnProperty(key)) {
|
|
60
|
+
const offering = offerings.all[key];
|
|
61
|
+
if (offering && offering.availablePackages?.length > 0) {
|
|
62
|
+
const pkg = offering.availablePackages[0];
|
|
63
|
+
let badge = '';
|
|
64
|
+
let pricePerWeek = '';
|
|
65
|
+
|
|
66
|
+
// Map plans according to receipt-scanner pattern
|
|
67
|
+
if (offering.serverDescription === 'Weekly') {
|
|
68
|
+
badge = 'Popular';
|
|
69
|
+
} else if (offering.serverDescription === 'Annual') {
|
|
70
|
+
badge = '80% OFF!';
|
|
71
|
+
// Get the weekly price from RevenueCat for annual plans
|
|
72
|
+
pricePerWeek = pkg.product.pricePerWeekString || '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
mappedPlans.push({
|
|
76
|
+
id: pkg.identifier,
|
|
77
|
+
title: offering.serverDescription === 'Weekly' ? '1 Week' :
|
|
78
|
+
offering.serverDescription === 'Annual' ? '1 Year' :
|
|
79
|
+
offering.serverDescription === 'Monthly' ? '1 Month' : offering.serverDescription,
|
|
80
|
+
price: pkg.product.priceString,
|
|
81
|
+
period: `per ${pkg.packageType.toLowerCase()}`,
|
|
82
|
+
badge,
|
|
83
|
+
pricePerWeek,
|
|
84
|
+
revenueCatPackage: pkg,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
// Use mock data for development
|
|
91
|
+
mappedPlans.push(
|
|
92
|
+
{
|
|
93
|
+
id: 'mock_weekly',
|
|
94
|
+
title: '1 Week',
|
|
95
|
+
price: '$4.99',
|
|
96
|
+
period: 'per week',
|
|
97
|
+
badge: 'Popular',
|
|
98
|
+
revenueCatPackage: null,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'mock_annual',
|
|
102
|
+
title: '1 Year',
|
|
103
|
+
price: '$49.99',
|
|
104
|
+
period: 'per year',
|
|
105
|
+
badge: '80% OFF!',
|
|
106
|
+
pricePerWeek: '$0.96',
|
|
107
|
+
revenueCatPackage: null,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'mock_monthly',
|
|
111
|
+
title: '1 Month',
|
|
112
|
+
price: '$9.99',
|
|
113
|
+
period: 'per month',
|
|
114
|
+
revenueCatPackage: null,
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Sort: Weekly, Annual, Monthly
|
|
120
|
+
mappedPlans.sort((a, b) => {
|
|
121
|
+
const order = ['1 Week', '1 Year', '1 Month'];
|
|
122
|
+
return order.indexOf(a.title) - order.indexOf(b.title);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return mappedPlans;
|
|
126
|
+
}, [offerings]);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (plans.length > 0 && !selectedPlan) {
|
|
130
|
+
// Select the Annual plan by default (80% OFF!)
|
|
131
|
+
const annualPlan = plans.find(p => p.title === '1 Year') || plans[0];
|
|
132
|
+
setSelectedPlan(annualPlan.id);
|
|
133
|
+
}
|
|
134
|
+
}, [plans, selectedPlan]);
|
|
135
|
+
|
|
136
|
+
const handlePurchase = async () => {
|
|
137
|
+
if (!selectedPlan) return;
|
|
138
|
+
|
|
139
|
+
const plan = plans.find(p => p.id === selectedPlan);
|
|
140
|
+
if (!plan || !plan.revenueCatPackage) {
|
|
141
|
+
Alert.alert('Error', 'Unable to process subscription. Please try again later.');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
setIsLoading(true);
|
|
147
|
+
const result = await purchasePackage(plan.revenueCatPackage);
|
|
148
|
+
const success = result?.success;
|
|
149
|
+
|
|
150
|
+
if (success) {
|
|
151
|
+
Alert.alert(
|
|
152
|
+
'Welcome to Premium!',
|
|
153
|
+
'Your subscription is now active. You can now access all premium features!',
|
|
154
|
+
[
|
|
155
|
+
{
|
|
156
|
+
text: 'Start Using App',
|
|
157
|
+
onPress: async () => {
|
|
158
|
+
// Mark onboarding as completed when subscription is successful
|
|
159
|
+
await AsyncStorage.setItem('onboarding_completed', 'true');
|
|
160
|
+
logger.info('Subscription successful - onboarding marked as completed');
|
|
161
|
+
|
|
162
|
+
// Navigate to main app
|
|
163
|
+
router.replace('/(tabs)');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
Alert.alert('Error', 'Failed to activate subscription. Please try again.');
|
|
170
|
+
}
|
|
171
|
+
} catch (e: any) {
|
|
172
|
+
if (!e.userCancelled) {
|
|
173
|
+
console.error('Subscription activation error:', e);
|
|
174
|
+
Alert.alert('Error', 'Something went wrong. Please try again.');
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
setIsLoading(false);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handleSkip = async () => {
|
|
182
|
+
try {
|
|
183
|
+
// Mark onboarding as completed when user skips paywall
|
|
184
|
+
await AsyncStorage.setItem('onboarding_completed', 'true');
|
|
185
|
+
logger.info('Paywall skipped - onboarding marked as completed');
|
|
186
|
+
|
|
187
|
+
// Navigate to main app
|
|
188
|
+
router.replace('/(tabs)');
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.error('Error completing onboarding after skip', { error });
|
|
191
|
+
Alert.alert('Error', 'Something went wrong. Please restart the app.');
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handleRestore = async () => {
|
|
196
|
+
try {
|
|
197
|
+
setIsLoading(true);
|
|
198
|
+
await restorePurchases();
|
|
199
|
+
Alert.alert('Success', 'Your purchases have been restored successfully.');
|
|
200
|
+
} catch (error) {
|
|
201
|
+
Alert.alert('Error', 'Failed to restore purchases. Please try again.');
|
|
202
|
+
} finally {
|
|
203
|
+
setIsLoading(false);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const renderPlanCard = (plan: PlanOption) => {
|
|
208
|
+
const isSelected = selectedPlan === plan.id;
|
|
209
|
+
const hasPopularBadge = plan.badge === 'Popular';
|
|
210
|
+
const hasDiscountBadge = plan.badge === '80% OFF!';
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<TouchableOpacity
|
|
214
|
+
key={plan.id}
|
|
215
|
+
style={[
|
|
216
|
+
styles.planCard,
|
|
217
|
+
hasPopularBadge && !isSelected && styles.popularPlan,
|
|
218
|
+
hasDiscountBadge && !isSelected && styles.discountPlan,
|
|
219
|
+
isSelected && styles.selectedPlan,
|
|
220
|
+
]}
|
|
221
|
+
onPress={() => setSelectedPlan(plan.id)}
|
|
222
|
+
activeOpacity={0.7}
|
|
223
|
+
>
|
|
224
|
+
{plan.badge && (
|
|
225
|
+
<View style={[
|
|
226
|
+
styles.badge,
|
|
227
|
+
hasPopularBadge && styles.popularBadge,
|
|
228
|
+
hasDiscountBadge && styles.discountBadge,
|
|
229
|
+
]}>
|
|
230
|
+
<Text style={styles.badgeText}>{plan.badge}</Text>
|
|
231
|
+
</View>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
<View style={styles.planContent}>
|
|
235
|
+
<Text style={styles.planTitle}>{plan.title}</Text>
|
|
236
|
+
<View style={styles.planPriceContainer}>
|
|
237
|
+
<Text style={styles.planPrice}>{plan.price}</Text>
|
|
238
|
+
{plan.pricePerWeek && (
|
|
239
|
+
<View style={styles.weeklyPriceContainer}>
|
|
240
|
+
<Text style={styles.pricePerWeek}>{plan.pricePerWeek}</Text>
|
|
241
|
+
<Text style={styles.perWeekLabel}>per week</Text>
|
|
242
|
+
</View>
|
|
243
|
+
)}
|
|
244
|
+
</View>
|
|
245
|
+
</View>
|
|
246
|
+
</TouchableOpacity>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Image placeholder for children prop
|
|
251
|
+
const paywallContent = (
|
|
252
|
+
<View style={styles.imageContainer}>
|
|
253
|
+
<View style={styles.imagePlaceholder} />
|
|
254
|
+
</View>
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Middle content: subscription plans with optional warning banner
|
|
258
|
+
const middleContent = (
|
|
259
|
+
<View style={styles.middleContentContainer}>
|
|
260
|
+
{!hasOfferings && (
|
|
261
|
+
<View style={styles.warningBanner}>
|
|
262
|
+
<IconSymbol name="exclamationmark.triangle.fill" size={16} color="#F59E0B" />
|
|
263
|
+
<Text style={styles.warningText}>
|
|
264
|
+
Test Mode: Showing mock pricing. Configure RevenueCat to see real offerings.
|
|
265
|
+
</Text>
|
|
266
|
+
</View>
|
|
267
|
+
)}
|
|
268
|
+
<View style={styles.plansContainer}>
|
|
269
|
+
{plans.map(renderPlanCard)}
|
|
270
|
+
</View>
|
|
271
|
+
</View>
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Footer content: billing info and links
|
|
275
|
+
const footerContent = (
|
|
276
|
+
<>
|
|
277
|
+
<View style={styles.billingInfo}>
|
|
278
|
+
<View style={styles.billingRow}>
|
|
279
|
+
<IconSymbol name="checkmark" size={16} color="#4CAF50" />
|
|
280
|
+
<Text style={styles.billingText}>Recurring billing, cancel anytime</Text>
|
|
281
|
+
</View>
|
|
282
|
+
</View>
|
|
283
|
+
|
|
284
|
+
<View style={styles.footerLinks}>
|
|
285
|
+
<TouchableOpacity onPress={() => {}}>
|
|
286
|
+
<Text style={styles.footerLinkText}>Terms</Text>
|
|
287
|
+
</TouchableOpacity>
|
|
288
|
+
<TouchableOpacity onPress={() => {}}>
|
|
289
|
+
<Text style={styles.footerLinkText}>Privacy</Text>
|
|
290
|
+
</TouchableOpacity>
|
|
291
|
+
<TouchableOpacity onPress={handleRestore}>
|
|
292
|
+
<Text style={styles.footerLinkText}>Restore</Text>
|
|
293
|
+
</TouchableOpacity>
|
|
294
|
+
</View>
|
|
295
|
+
</>
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Loading content when subscription is loading
|
|
299
|
+
const loadingContent = subscriptionLoading ? (
|
|
300
|
+
<>
|
|
301
|
+
<ActivityIndicator size="large" color={Theme.colors.primary} />
|
|
302
|
+
<Text style={styles.loadingText}>Loading subscription details...</Text>
|
|
303
|
+
</>
|
|
304
|
+
) : undefined;
|
|
305
|
+
|
|
306
|
+
// Show subscription management if already subscribed
|
|
307
|
+
if (hasActiveSubscription) {
|
|
308
|
+
const subscriptionContent = (
|
|
309
|
+
<View style={styles.imageContainer}>
|
|
310
|
+
<View style={styles.imagePlaceholder} />
|
|
311
|
+
<View style={styles.subscribedIconOverlay}>
|
|
312
|
+
<IconSymbol
|
|
313
|
+
name="checkmark.circle.fill"
|
|
314
|
+
size={80}
|
|
315
|
+
color="#4CAF50"
|
|
316
|
+
/>
|
|
317
|
+
</View>
|
|
318
|
+
</View>
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<>
|
|
323
|
+
<Stack.Screen options={{ headerShown: false }} />
|
|
324
|
+
<OnboardingLayout
|
|
325
|
+
title="You're All Set!"
|
|
326
|
+
subtitle="You already have an active subscription. Enjoy all premium features!"
|
|
327
|
+
onContinue={async () => {
|
|
328
|
+
// Mark onboarding as completed for users with existing subscriptions
|
|
329
|
+
await AsyncStorage.setItem('onboarding_completed', 'true');
|
|
330
|
+
logger.info('Existing subscription detected - onboarding marked as completed');
|
|
331
|
+
|
|
332
|
+
// Navigate to main app
|
|
333
|
+
router.replace('/(tabs)');
|
|
334
|
+
}}
|
|
335
|
+
continueText="Continue to App"
|
|
336
|
+
pageIndicators={4}
|
|
337
|
+
totalPages={4}
|
|
338
|
+
>
|
|
339
|
+
{subscriptionContent}
|
|
340
|
+
</OnboardingLayout>
|
|
341
|
+
</>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<>
|
|
347
|
+
<Stack.Screen options={{ headerShown: false }} />
|
|
348
|
+
<OnboardingLayout
|
|
349
|
+
title="Premium"
|
|
350
|
+
subtitle="Unlock all premium features and advanced functionality"
|
|
351
|
+
pageIndicators={4}
|
|
352
|
+
totalPages={4}
|
|
353
|
+
onContinue={handlePurchase}
|
|
354
|
+
continueDisabled={!selectedPlan || isLoading}
|
|
355
|
+
continueLoading={isLoading}
|
|
356
|
+
loadingContent={loadingContent}
|
|
357
|
+
middleContent={!subscriptionLoading ? middleContent : undefined}
|
|
358
|
+
footerContent={!subscriptionLoading ? footerContent : undefined}
|
|
359
|
+
onSkip={handleSkip}
|
|
360
|
+
showSkipAfter={3}
|
|
361
|
+
>
|
|
362
|
+
{paywallContent}
|
|
363
|
+
</OnboardingLayout>
|
|
364
|
+
</>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const imageSize = responsive.getImageContainerSize();
|
|
369
|
+
const planCardHeight = responsive.getPlanCardHeight();
|
|
370
|
+
|
|
371
|
+
const styles = StyleSheet.create({
|
|
372
|
+
// Image Section
|
|
373
|
+
imageContainer: {
|
|
374
|
+
width: imageSize.width,
|
|
375
|
+
height: imageSize.height,
|
|
376
|
+
justifyContent: 'center',
|
|
377
|
+
alignItems: 'center',
|
|
378
|
+
},
|
|
379
|
+
imagePlaceholder: {
|
|
380
|
+
width: '100%',
|
|
381
|
+
height: '100%',
|
|
382
|
+
backgroundColor: Theme.colors.backgroundSecondary,
|
|
383
|
+
borderRadius: responsive.scale(20),
|
|
384
|
+
borderWidth: 2,
|
|
385
|
+
borderColor: Theme.colors.borderLight,
|
|
386
|
+
borderStyle: 'dashed',
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
// Middle Content Container
|
|
390
|
+
middleContentContainer: {
|
|
391
|
+
width: '100%',
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
// Warning Banner
|
|
395
|
+
warningBanner: {
|
|
396
|
+
flexDirection: 'row',
|
|
397
|
+
alignItems: 'center',
|
|
398
|
+
backgroundColor: '#FEF3C7',
|
|
399
|
+
borderWidth: 1,
|
|
400
|
+
borderColor: '#F59E0B',
|
|
401
|
+
borderRadius: responsive.scale(8),
|
|
402
|
+
padding: responsive.scale(10),
|
|
403
|
+
marginBottom: getSpacing(12),
|
|
404
|
+
gap: getSpacing(8),
|
|
405
|
+
},
|
|
406
|
+
warningText: {
|
|
407
|
+
flex: 1,
|
|
408
|
+
fontSize: fontSize(12),
|
|
409
|
+
color: '#92400E',
|
|
410
|
+
fontWeight: '500',
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
// Plans Section - Horizontal Layout
|
|
414
|
+
plansContainer: {
|
|
415
|
+
flexDirection: 'row',
|
|
416
|
+
gap: responsive.scale(8),
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
planCard: {
|
|
420
|
+
flex: 1,
|
|
421
|
+
backgroundColor: '#F8F9FA',
|
|
422
|
+
borderRadius: responsive.scale(12),
|
|
423
|
+
padding: responsive.scale(10),
|
|
424
|
+
borderWidth: responsive.scale(1.5),
|
|
425
|
+
borderColor: '#E5E7EB',
|
|
426
|
+
position: 'relative',
|
|
427
|
+
alignItems: 'center',
|
|
428
|
+
height: planCardHeight,
|
|
429
|
+
justifyContent: 'center',
|
|
430
|
+
},
|
|
431
|
+
selectedPlan: {
|
|
432
|
+
borderColor: Theme.colors.primary,
|
|
433
|
+
backgroundColor: `${Theme.colors.primary}14`, // Light blue tint (14 = 8% opacity in hex)
|
|
434
|
+
borderWidth: responsive.scale(2.5),
|
|
435
|
+
shadowColor: Theme.colors.primary,
|
|
436
|
+
shadowOffset: { width: 0, height: 0 },
|
|
437
|
+
shadowOpacity: 0.3,
|
|
438
|
+
shadowRadius: responsive.scale(8),
|
|
439
|
+
elevation: 5,
|
|
440
|
+
},
|
|
441
|
+
popularPlan: {
|
|
442
|
+
borderColor: Theme.colors.primary,
|
|
443
|
+
},
|
|
444
|
+
discountPlan: {
|
|
445
|
+
borderColor: Theme.colors.primary,
|
|
446
|
+
},
|
|
447
|
+
badge: {
|
|
448
|
+
position: 'absolute',
|
|
449
|
+
top: responsive.scale(-8),
|
|
450
|
+
backgroundColor: Theme.colors.primary,
|
|
451
|
+
paddingHorizontal: getSpacing(8),
|
|
452
|
+
paddingVertical: getSpacing(3),
|
|
453
|
+
borderRadius: responsive.scale(8),
|
|
454
|
+
zIndex: 1,
|
|
455
|
+
},
|
|
456
|
+
popularBadge: {
|
|
457
|
+
backgroundColor: Theme.colors.primary,
|
|
458
|
+
},
|
|
459
|
+
discountBadge: {
|
|
460
|
+
backgroundColor: Theme.colors.primary,
|
|
461
|
+
},
|
|
462
|
+
badgeText: {
|
|
463
|
+
color: Theme.colors.textInverse,
|
|
464
|
+
fontSize: fontSize(10),
|
|
465
|
+
fontWeight: '700',
|
|
466
|
+
},
|
|
467
|
+
planContent: {
|
|
468
|
+
alignItems: 'center',
|
|
469
|
+
justifyContent: 'flex-start',
|
|
470
|
+
flex: 1,
|
|
471
|
+
paddingTop: getSpacing(12),
|
|
472
|
+
},
|
|
473
|
+
planTitle: {
|
|
474
|
+
fontSize: fontSize(14),
|
|
475
|
+
fontWeight: '600',
|
|
476
|
+
color: Theme.colors.text,
|
|
477
|
+
marginBottom: getSpacing(4),
|
|
478
|
+
},
|
|
479
|
+
planPriceContainer: {
|
|
480
|
+
alignItems: 'center',
|
|
481
|
+
},
|
|
482
|
+
planPrice: {
|
|
483
|
+
fontSize: fontSize(14),
|
|
484
|
+
fontWeight: '600',
|
|
485
|
+
color: Theme.colors.text,
|
|
486
|
+
},
|
|
487
|
+
weeklyPriceContainer: {
|
|
488
|
+
backgroundColor: Theme.colors.primary,
|
|
489
|
+
borderRadius: responsive.scale(6),
|
|
490
|
+
paddingHorizontal: getSpacing(8),
|
|
491
|
+
paddingVertical: getSpacing(4),
|
|
492
|
+
marginTop: getSpacing(8),
|
|
493
|
+
flexDirection: 'column',
|
|
494
|
+
alignItems: 'center',
|
|
495
|
+
},
|
|
496
|
+
pricePerWeek: {
|
|
497
|
+
fontSize: fontSize(12),
|
|
498
|
+
color: Theme.colors.textInverse,
|
|
499
|
+
fontWeight: '600',
|
|
500
|
+
},
|
|
501
|
+
perWeekLabel: {
|
|
502
|
+
fontSize: fontSize(11),
|
|
503
|
+
color: Theme.colors.textInverse,
|
|
504
|
+
fontWeight: '400',
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
// Billing Info
|
|
508
|
+
billingInfo: {
|
|
509
|
+
alignItems: 'center',
|
|
510
|
+
marginBottom: getSpacing(10),
|
|
511
|
+
},
|
|
512
|
+
billingRow: {
|
|
513
|
+
flexDirection: 'row',
|
|
514
|
+
alignItems: 'center',
|
|
515
|
+
gap: getSpacing(6),
|
|
516
|
+
},
|
|
517
|
+
billingText: {
|
|
518
|
+
fontSize: fontSize(13),
|
|
519
|
+
color: Theme.colors.textSecondary,
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
// Footer Links
|
|
523
|
+
footerLinks: {
|
|
524
|
+
flexDirection: 'row',
|
|
525
|
+
justifyContent: 'center',
|
|
526
|
+
gap: getSpacing(28),
|
|
527
|
+
},
|
|
528
|
+
footerLinkText: {
|
|
529
|
+
fontSize: fontSize(13),
|
|
530
|
+
color: Theme.colors.textSecondary,
|
|
531
|
+
textDecorationLine: 'underline',
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
// Loading state
|
|
535
|
+
loadingText: {
|
|
536
|
+
fontSize: fontSize(16),
|
|
537
|
+
color: Theme.colors.textSecondary,
|
|
538
|
+
marginTop: getSpacing(16),
|
|
539
|
+
textAlign: 'center',
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
// Subscribed state
|
|
543
|
+
subscribedIconOverlay: {
|
|
544
|
+
position: 'absolute',
|
|
545
|
+
justifyContent: 'center',
|
|
546
|
+
alignItems: 'center',
|
|
547
|
+
width: '100%',
|
|
548
|
+
height: '100%',
|
|
549
|
+
},
|
|
550
|
+
});
|