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.
Files changed (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +642 -0
  3. package/bin/cli.js +12 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +113 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config/dependencies.d.ts +82 -0
  9. package/dist/config/dependencies.d.ts.map +1 -0
  10. package/dist/config/dependencies.js +82 -0
  11. package/dist/config/dependencies.js.map +1 -0
  12. package/dist/config/presets.d.ts +3 -0
  13. package/dist/config/presets.d.ts.map +1 -0
  14. package/dist/config/presets.js +174 -0
  15. package/dist/config/presets.js.map +1 -0
  16. package/dist/generators/index.d.ts +40 -0
  17. package/dist/generators/index.d.ts.map +1 -0
  18. package/dist/generators/index.js +130 -0
  19. package/dist/generators/index.js.map +1 -0
  20. package/dist/generators/onboarding.d.ts +8 -0
  21. package/dist/generators/onboarding.d.ts.map +1 -0
  22. package/dist/generators/onboarding.js +141 -0
  23. package/dist/generators/onboarding.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +65 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/prompts/features.d.ts +14 -0
  29. package/dist/prompts/features.d.ts.map +1 -0
  30. package/dist/prompts/features.js +96 -0
  31. package/dist/prompts/features.js.map +1 -0
  32. package/dist/prompts/index.d.ts +3 -0
  33. package/dist/prompts/index.d.ts.map +1 -0
  34. package/dist/prompts/index.js +93 -0
  35. package/dist/prompts/index.js.map +1 -0
  36. package/dist/prompts/onboarding.d.ts +6 -0
  37. package/dist/prompts/onboarding.d.ts.map +1 -0
  38. package/dist/prompts/onboarding.js +37 -0
  39. package/dist/prompts/onboarding.js.map +1 -0
  40. package/dist/prompts/orm.d.ts +3 -0
  41. package/dist/prompts/orm.d.ts.map +1 -0
  42. package/dist/prompts/orm.js +23 -0
  43. package/dist/prompts/orm.js.map +1 -0
  44. package/dist/prompts/packageManager.d.ts +2 -0
  45. package/dist/prompts/packageManager.d.ts.map +1 -0
  46. package/dist/prompts/packageManager.js +18 -0
  47. package/dist/prompts/packageManager.js.map +1 -0
  48. package/dist/prompts/platform.d.ts +3 -0
  49. package/dist/prompts/platform.d.ts.map +1 -0
  50. package/dist/prompts/platform.js +21 -0
  51. package/dist/prompts/platform.js.map +1 -0
  52. package/dist/prompts/preset.d.ts +4 -0
  53. package/dist/prompts/preset.d.ts.map +1 -0
  54. package/dist/prompts/preset.js +165 -0
  55. package/dist/prompts/preset.js.map +1 -0
  56. package/dist/prompts/project.d.ts +2 -0
  57. package/dist/prompts/project.d.ts.map +1 -0
  58. package/dist/prompts/project.js +27 -0
  59. package/dist/prompts/project.js.map +1 -0
  60. package/dist/prompts/sdks.d.ts +2 -0
  61. package/dist/prompts/sdks.d.ts.map +1 -0
  62. package/dist/prompts/sdks.js +46 -0
  63. package/dist/prompts/sdks.js.map +1 -0
  64. package/dist/types/index.d.ts +77 -0
  65. package/dist/types/index.d.ts.map +1 -0
  66. package/dist/types/index.js +25 -0
  67. package/dist/types/index.js.map +1 -0
  68. package/dist/utils/cleanup.d.ts +5 -0
  69. package/dist/utils/cleanup.d.ts.map +1 -0
  70. package/dist/utils/cleanup.js +38 -0
  71. package/dist/utils/cleanup.js.map +1 -0
  72. package/dist/utils/copy.d.ts +10 -0
  73. package/dist/utils/copy.d.ts.map +1 -0
  74. package/dist/utils/copy.js +53 -0
  75. package/dist/utils/copy.js.map +1 -0
  76. package/dist/utils/errors.d.ts +33 -0
  77. package/dist/utils/errors.d.ts.map +1 -0
  78. package/dist/utils/errors.js +136 -0
  79. package/dist/utils/errors.js.map +1 -0
  80. package/dist/utils/git.d.ts +5 -0
  81. package/dist/utils/git.d.ts.map +1 -0
  82. package/dist/utils/git.js +33 -0
  83. package/dist/utils/git.js.map +1 -0
  84. package/dist/utils/logger.d.ts +9 -0
  85. package/dist/utils/logger.d.ts.map +1 -0
  86. package/dist/utils/logger.js +22 -0
  87. package/dist/utils/logger.js.map +1 -0
  88. package/dist/utils/package.d.ts +16 -0
  89. package/dist/utils/package.d.ts.map +1 -0
  90. package/dist/utils/package.js +86 -0
  91. package/dist/utils/package.js.map +1 -0
  92. package/dist/utils/system-validation.d.ts +9 -0
  93. package/dist/utils/system-validation.d.ts.map +1 -0
  94. package/dist/utils/system-validation.js +31 -0
  95. package/dist/utils/system-validation.js.map +1 -0
  96. package/dist/utils/template.d.ts +20 -0
  97. package/dist/utils/template.d.ts.map +1 -0
  98. package/dist/utils/template.js +234 -0
  99. package/dist/utils/template.js.map +1 -0
  100. package/dist/utils/validation.d.ts +8 -0
  101. package/dist/utils/validation.d.ts.map +1 -0
  102. package/dist/utils/validation.js +94 -0
  103. package/dist/utils/validation.js.map +1 -0
  104. package/package.json +96 -0
  105. package/templates/base/backend/.dockerignore.ejs +62 -0
  106. package/templates/base/backend/.env.example.ejs +116 -0
  107. package/templates/base/backend/Dockerfile.ejs +142 -0
  108. package/templates/base/backend/controllers/event-queue/index.ts +20 -0
  109. package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
  110. package/templates/base/backend/controllers/rest-api/index.ts +48 -0
  111. package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
  112. package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
  113. package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
  114. package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
  115. package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
  116. package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
  117. package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
  118. package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
  119. package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
  120. package/templates/base/backend/domain/device-session/schema.ts +72 -0
  121. package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
  122. package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
  123. package/templates/base/backend/domain/session/schema.ts +29 -0
  124. package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
  125. package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
  126. package/templates/base/backend/domain/user/schema.ts +14 -0
  127. package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
  128. package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
  129. package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
  130. package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
  131. package/templates/base/backend/lib/constants.ts.ejs +29 -0
  132. package/templates/base/backend/package.json.ejs +50 -0
  133. package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
  134. package/templates/base/backend/prisma.config.prisma.ts +12 -0
  135. package/templates/base/backend/tsconfig.json +39 -0
  136. package/templates/base/backend/utils/db.drizzle.ts +41 -0
  137. package/templates/base/backend/utils/db.prisma.ts +51 -0
  138. package/templates/base/backend/utils/email.ts.ejs +35 -0
  139. package/templates/base/backend/utils/errors.ts +348 -0
  140. package/templates/base/backend/utils/redis.ts.ejs +279 -0
  141. package/templates/base/mobile/.env.example.ejs +35 -0
  142. package/templates/base/mobile/.gitignore.ejs +167 -0
  143. package/templates/base/mobile/app/+not-found.tsx +85 -0
  144. package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
  145. package/templates/base/mobile/app.json.ejs +88 -0
  146. package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
  147. package/templates/base/mobile/assets/images/favicon.png +0 -0
  148. package/templates/base/mobile/assets/images/icon.png +0 -0
  149. package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
  150. package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
  151. package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
  152. package/templates/base/mobile/assets/images/paywall_image.png +0 -0
  153. package/templates/base/mobile/assets/images/splash.png +0 -0
  154. package/templates/base/mobile/eas.json.ejs +49 -0
  155. package/templates/base/mobile/metro.config.js +9 -0
  156. package/templates/base/mobile/package.json.ejs +53 -0
  157. package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
  158. package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
  159. package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
  160. package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
  161. package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
  162. package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
  163. package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
  164. package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
  165. package/templates/base/mobile/src/components/ui/index.ts +6 -0
  166. package/templates/base/mobile/src/constants/Theme.ts +163 -0
  167. package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
  168. package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
  169. package/templates/base/mobile/src/services/api.ts.ejs +71 -0
  170. package/templates/base/mobile/src/services/errorService.ts +179 -0
  171. package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
  172. package/templates/base/mobile/src/store/index.ts.ejs +18 -0
  173. package/templates/base/mobile/src/store/ui.store.ts +100 -0
  174. package/templates/base/mobile/src/utils/formatters.ts +105 -0
  175. package/templates/base/mobile/src/utils/logger.ts +73 -0
  176. package/templates/base/mobile/src/utils/responsive.ts +234 -0
  177. package/templates/base/mobile/tsconfig.json +32 -0
  178. package/templates/base/web/.env.example.ejs +26 -0
  179. package/templates/base/web/components.json +22 -0
  180. package/templates/base/web/eslint.config.mjs +18 -0
  181. package/templates/base/web/next.config.ts +7 -0
  182. package/templates/base/web/package.json.ejs +35 -0
  183. package/templates/base/web/postcss.config.mjs +7 -0
  184. package/templates/base/web/public/.gitkeep +0 -0
  185. package/templates/base/web/public/file.svg +1 -0
  186. package/templates/base/web/public/globe.svg +1 -0
  187. package/templates/base/web/public/next.svg +1 -0
  188. package/templates/base/web/public/vercel.svg +1 -0
  189. package/templates/base/web/public/window.svg +1 -0
  190. package/templates/base/web/src/app/favicon.ico +0 -0
  191. package/templates/base/web/src/app/globals.css +152 -0
  192. package/templates/base/web/src/app/layout.tsx.ejs +54 -0
  193. package/templates/base/web/src/app/page.tsx.ejs +92 -0
  194. package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
  195. package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
  196. package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
  197. package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
  198. package/templates/base/web/src/components/theme-toggle.tsx +34 -0
  199. package/templates/base/web/src/components/ui/button.tsx +62 -0
  200. package/templates/base/web/src/components/ui/card.tsx +92 -0
  201. package/templates/base/web/src/components/ui/input.tsx +21 -0
  202. package/templates/base/web/src/components/ui/label.tsx +24 -0
  203. package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
  204. package/templates/base/web/src/components/ui/spinner.tsx +20 -0
  205. package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
  206. package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
  207. package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
  208. package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
  209. package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
  210. package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
  211. package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
  212. package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
  213. package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
  214. package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
  215. package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
  216. package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
  217. package/templates/base/web/src/lib/utils.ts +6 -0
  218. package/templates/base/web/src/proxy.ts.ejs +66 -0
  219. package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
  220. package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
  221. package/templates/base/web/tsconfig.json +34 -0
  222. package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
  223. package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
  224. package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
  225. package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
  226. package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
  227. package/templates/features/mobile/auth/components/auth/index.ts +2 -0
  228. package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
  229. package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
  230. package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
  231. package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
  232. package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
  233. package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
  234. package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
  235. package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
  236. package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
  237. package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
  238. package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
  239. package/templates/features/web/.gitkeep +0 -0
  240. package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
  241. package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
  242. package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
  243. package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
  244. package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
  245. package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
  246. package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
  247. package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
  248. package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
  249. package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
  250. package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
  251. package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
  252. package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
  253. package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
  254. package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
  255. package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
  256. package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
  257. package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
  258. package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
  259. package/templates/integrations/mobile/att/services/attService.ts +84 -0
  260. package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
  261. package/templates/integrations/mobile/att/store/att.store.ts +162 -0
  262. package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
  263. package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
  264. package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
  265. package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
  266. package/templates/integrations/web/.gitkeep +0 -0
  267. package/templates/shared/.env.example.ejs +21 -0
  268. package/templates/shared/.gitignore.ejs +145 -0
  269. package/templates/shared/README.md.ejs +134 -0
  270. package/templates/shared/docker-compose.prod.yml.ejs +120 -0
  271. package/templates/shared/docker-compose.yml.ejs +129 -0
  272. package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
  273. package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
  274. package/templates/shared/scripts/setup.sh.ejs +979 -0
@@ -0,0 +1,98 @@
1
+ import React from 'react';
2
+ import {
3
+ View,
4
+ ActivityIndicator,
5
+ Text,
6
+ StyleSheet,
7
+ ViewStyle,
8
+ TextStyle,
9
+ } from 'react-native';
10
+ import { useAppTheme } from '@/context/ThemeContext';
11
+
12
+ interface LoadingSpinnerProps {
13
+ size?: 'small' | 'large';
14
+ color?: string;
15
+ text?: string;
16
+ overlay?: boolean;
17
+ style?: ViewStyle;
18
+ textStyle?: TextStyle;
19
+ }
20
+
21
+ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
22
+ size = 'large',
23
+ color,
24
+ text,
25
+ overlay = false,
26
+ style,
27
+ textStyle,
28
+ }) => {
29
+ const theme = useAppTheme();
30
+
31
+ // Use provided color or fall back to theme primary
32
+ const spinnerColor = color ?? theme.colors.primary;
33
+
34
+ // Dynamic styles based on theme
35
+ const overlayStyle = {
36
+ backgroundColor: theme.mode === 'dark'
37
+ ? 'rgba(9, 9, 11, 0.9)' // neutral-950 with opacity
38
+ : 'rgba(255, 255, 255, 0.9)',
39
+ borderRadius: theme.borderRadius.lg,
40
+ };
41
+
42
+ const overlayBackgroundStyle = {
43
+ backgroundColor: theme.mode === 'dark'
44
+ ? 'rgba(0, 0, 0, 0.5)'
45
+ : 'rgba(0, 0, 0, 0.3)',
46
+ };
47
+
48
+ const containerStyle = [
49
+ styles.container,
50
+ overlay && [styles.overlay, overlayStyle],
51
+ style,
52
+ ];
53
+
54
+ const Wrapper = overlay ? View : React.Fragment;
55
+ const wrapperProps = overlay
56
+ ? { style: [styles.overlayBackground, overlayBackgroundStyle] }
57
+ : {};
58
+
59
+ return (
60
+ <Wrapper {...wrapperProps}>
61
+ <View style={containerStyle}>
62
+ <ActivityIndicator size={size} color={spinnerColor} />
63
+ {text && (
64
+ <Text style={[styles.text, { color: spinnerColor }, textStyle]}>
65
+ {text}
66
+ </Text>
67
+ )}
68
+ </View>
69
+ </Wrapper>
70
+ );
71
+ };
72
+
73
+ const styles = StyleSheet.create({
74
+ container: {
75
+ justifyContent: 'center',
76
+ alignItems: 'center',
77
+ padding: 20,
78
+ },
79
+
80
+ overlay: {
81
+ minWidth: 100,
82
+ minHeight: 100,
83
+ },
84
+
85
+ overlayBackground: {
86
+ ...StyleSheet.absoluteFillObject,
87
+ justifyContent: 'center',
88
+ alignItems: 'center',
89
+ zIndex: 1000,
90
+ },
91
+
92
+ text: {
93
+ marginTop: 12,
94
+ fontSize: 16,
95
+ fontWeight: '500',
96
+ textAlign: 'center',
97
+ },
98
+ });
@@ -0,0 +1,356 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ SafeAreaView,
7
+ TouchableOpacity,
8
+ ScrollView,
9
+ KeyboardAvoidingView,
10
+ Platform,
11
+ } from 'react-native';
12
+ import { IconSymbol } from './IconSymbol';
13
+ import { useAppTheme, AppTheme } from '@/context/ThemeContext';
14
+ import { responsive, fontSize, getSpacing } from '@/utils/responsive';
15
+
16
+ interface OnboardingLayoutProps {
17
+ children?: React.ReactNode; // For the image/content area
18
+ title: string;
19
+ subtitle: string;
20
+ pageIndicators?: number; // Current page (1-4)
21
+ totalPages?: number; // Total pages (3 for onboarding, 4 for paywall)
22
+ onContinue: () => void;
23
+ continueText?: string;
24
+ footerContent?: React.ReactNode; // For billing info and footer links in paywall
25
+ continueDisabled?: boolean;
26
+ continueLoading?: boolean;
27
+ loadingContent?: React.ReactNode;
28
+ middleContent?: React.ReactNode; // For subscription plans in paywall
29
+ onSkip?: () => void; // Skip functionality
30
+ showSkipAfter?: number; // Show skip button after X seconds (default 3)
31
+ }
32
+
33
+ export default function OnboardingLayout({
34
+ children,
35
+ title,
36
+ subtitle,
37
+ pageIndicators,
38
+ totalPages = 3,
39
+ onContinue,
40
+ continueText = 'Continue',
41
+ footerContent,
42
+ continueDisabled = false,
43
+ continueLoading = false,
44
+ loadingContent,
45
+ middleContent,
46
+ onSkip,
47
+ showSkipAfter = 3,
48
+ }: OnboardingLayoutProps) {
49
+ const theme = useAppTheme();
50
+ const styles = useMemo(() => createStyles(theme), [theme]);
51
+ const [showSkipButton, setShowSkipButton] = useState(false);
52
+
53
+ useEffect(() => {
54
+ if (onSkip) {
55
+ const timer = setTimeout(() => {
56
+ setShowSkipButton(true);
57
+ }, showSkipAfter * 1000);
58
+
59
+ return () => clearTimeout(timer);
60
+ }
61
+ }, [onSkip, showSkipAfter]);
62
+
63
+ return (
64
+ <SafeAreaView style={styles.container}>
65
+ <KeyboardAvoidingView
66
+ style={styles.keyboardAvoidingView}
67
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
68
+ >
69
+ {/* Skip Button */}
70
+ {showSkipButton && onSkip && (
71
+ <TouchableOpacity
72
+ style={styles.skipButton}
73
+ onPress={onSkip}
74
+ activeOpacity={0.7}
75
+ >
76
+ <IconSymbol
77
+ name="xmark"
78
+ size={responsive.moderateScale(20)}
79
+ color={theme.colors.textMuted}
80
+ />
81
+ </TouchableOpacity>
82
+ )}
83
+
84
+ <View style={styles.content}>
85
+ {loadingContent ? (
86
+ <View style={styles.loadingWrapper}>
87
+ {loadingContent}
88
+ </View>
89
+ ) : (
90
+ <>
91
+ {/* Scrollable content area */}
92
+ <ScrollView
93
+ style={styles.scrollView}
94
+ contentContainerStyle={styles.scrollContent}
95
+ showsVerticalScrollIndicator={false}
96
+ bounces={false}
97
+ >
98
+ {/* Top Section - Image/Content Area */}
99
+ <View style={styles.topSection}>
100
+ {children || (
101
+ <View style={styles.imagePlaceholder}>
102
+ {/* Default placeholder */}
103
+ </View>
104
+ )}
105
+ </View>
106
+
107
+ {/* Text Content */}
108
+ <View style={styles.textContainer}>
109
+ <Text style={styles.title}>{title}</Text>
110
+ <Text style={styles.subtitle}>{subtitle}</Text>
111
+ </View>
112
+
113
+ {/* Middle Content - Subscription Plans (only in paywall) */}
114
+ {middleContent && (
115
+ <View style={styles.middleContentWrapper}>
116
+ {middleContent}
117
+ </View>
118
+ )}
119
+
120
+ {/* Bottom padding to ensure content doesn't overlap with fixed bottom */}
121
+ <View style={styles.bottomPadding} />
122
+ </ScrollView>
123
+
124
+ {/* Fixed Bottom Content - Background and spacing */}
125
+ <View style={[
126
+ styles.fixedBottomContent,
127
+ footerContent ? styles.fixedBottomContentWithFooter : styles.fixedBottomContentNoFooter
128
+ ]}>
129
+ {/* Page Indicators */}
130
+ {pageIndicators && (
131
+ <View style={styles.pageIndicators}>
132
+ {Array.from({ length: totalPages }, (_, index) => (
133
+ <View
134
+ key={index}
135
+ style={[
136
+ styles.indicator,
137
+ pageIndicators === index + 1 && styles.indicatorActive,
138
+ ]}
139
+ />
140
+ ))}
141
+ </View>
142
+ )}
143
+
144
+ {/* Footer Content (billing info, links) - positioned at bottom */}
145
+ {footerContent && (
146
+ <View style={styles.footerContentWrapper}>
147
+ {footerContent}
148
+ </View>
149
+ )}
150
+ </View>
151
+
152
+ {/* Continue Button - Fixed position from bottom */}
153
+ <View style={styles.buttonContainer}>
154
+ <TouchableOpacity
155
+ style={[
156
+ styles.continueButton,
157
+ (continueDisabled || continueLoading) && styles.continueButtonDisabled
158
+ ]}
159
+ onPress={onContinue}
160
+ activeOpacity={continueDisabled || continueLoading ? 1 : 0.7}
161
+ disabled={continueDisabled || continueLoading}
162
+ >
163
+ <View style={styles.buttonSpacer} />
164
+ <Text style={styles.continueButtonText}>{continueText}</Text>
165
+ <IconSymbol
166
+ name="arrow.right"
167
+ size={20}
168
+ color={theme.colors.textInverse}
169
+ />
170
+ </TouchableOpacity>
171
+ </View>
172
+ </>
173
+ )}
174
+ </View>
175
+ </KeyboardAvoidingView>
176
+ </SafeAreaView>
177
+ );
178
+ }
179
+
180
+ const createStyles = (theme: AppTheme) => {
181
+ const sectionHeights = responsive.getSectionHeights();
182
+ const imageSize = responsive.getImageContainerSize();
183
+
184
+ return StyleSheet.create({
185
+ container: {
186
+ flex: 1,
187
+ backgroundColor: theme.colors.background,
188
+ },
189
+ keyboardAvoidingView: {
190
+ flex: 1,
191
+ },
192
+ skipButton: {
193
+ position: 'absolute',
194
+ top: responsive.verticalScale(60),
195
+ right: getSpacing(24),
196
+ width: responsive.scale(32),
197
+ height: responsive.scale(32),
198
+ backgroundColor: theme.colors.backgroundSecondary,
199
+ borderRadius: responsive.scale(16),
200
+ justifyContent: 'center',
201
+ alignItems: 'center',
202
+ zIndex: 10,
203
+ ...theme.shadows.small,
204
+ },
205
+ content: {
206
+ flex: 1,
207
+ paddingHorizontal: getSpacing(24),
208
+ },
209
+
210
+ // ScrollView styles
211
+ scrollView: {
212
+ flex: 1,
213
+ },
214
+ scrollContent: {
215
+ flexGrow: 1,
216
+ paddingTop: getSpacing(16),
217
+ },
218
+
219
+ // Top Section - Image area
220
+ topSection: {
221
+ height: sectionHeights.topSection,
222
+ justifyContent: 'center',
223
+ alignItems: 'center',
224
+ minHeight: responsive.verticalScale(200), // Minimum height to prevent squishing
225
+ },
226
+
227
+ // Image placeholder
228
+ imagePlaceholder: {
229
+ width: imageSize.width,
230
+ height: imageSize.height,
231
+ backgroundColor: theme.colors.backgroundSecondary,
232
+ borderRadius: responsive.scale(20),
233
+ borderWidth: 2,
234
+ borderColor: theme.colors.borderLight,
235
+ borderStyle: 'dashed',
236
+ },
237
+
238
+ // Text content
239
+ textContainer: {
240
+ alignItems: 'center',
241
+ paddingHorizontal: responsive.scale(20),
242
+ marginBottom: responsive.scale(16),
243
+ },
244
+
245
+ title: {
246
+ fontSize: fontSize(24),
247
+ fontWeight: '700',
248
+ color: theme.colors.text,
249
+ textAlign: 'center',
250
+ lineHeight: fontSize(30),
251
+ marginTop: responsive.scale(-12),
252
+ marginBottom: responsive.scale(8),
253
+ },
254
+ subtitle: {
255
+ fontSize: fontSize(16),
256
+ color: theme.colors.textSecondary,
257
+ textAlign: 'center',
258
+ lineHeight: fontSize(22),
259
+ },
260
+
261
+ // Middle content wrapper (for subscription plans)
262
+ middleContentWrapper: {
263
+ marginTop: getSpacing(16),
264
+ marginBottom: getSpacing(12),
265
+ flex: 1,
266
+ justifyContent: 'center',
267
+ },
268
+
269
+ // Bottom padding for ScrollView to prevent content overlap with fixed bottom
270
+ bottomPadding: {
271
+ height: sectionHeights.bottomPadding,
272
+ },
273
+
274
+ // Fixed bottom content container
275
+ fixedBottomContent: {
276
+ position: 'absolute',
277
+ bottom: 0,
278
+ left: 0,
279
+ right: 0,
280
+ backgroundColor: theme.colors.background,
281
+ paddingHorizontal: getSpacing(24),
282
+ paddingTop: getSpacing(20),
283
+ },
284
+ fixedBottomContentWithFooter: {
285
+ paddingBottom: 0,
286
+ },
287
+ fixedBottomContentNoFooter: {
288
+ paddingBottom: getSpacing(20),
289
+ },
290
+
291
+ // Page indicators
292
+ pageIndicators: {
293
+ flexDirection: 'row',
294
+ justifyContent: 'center',
295
+ gap: responsive.scale(8),
296
+ marginBottom: sectionHeights.pageIndicatorMargin,
297
+ height: responsive.scale(8),
298
+ },
299
+ indicator: {
300
+ width: responsive.scale(8),
301
+ height: responsive.scale(8),
302
+ borderRadius: responsive.scale(4),
303
+ backgroundColor: theme.colors.borderLight,
304
+ },
305
+ indicatorActive: {
306
+ backgroundColor: theme.colors.primary,
307
+ width: responsive.scale(8), // Reverted to dot instead of pill
308
+ },
309
+
310
+ // Continue button - Fixed position from bottom
311
+ buttonContainer: {
312
+ position: 'absolute',
313
+ bottom: responsive.getButtonBottomPosition(),
314
+ left: getSpacing(24),
315
+ right: getSpacing(24),
316
+ },
317
+ continueButton: {
318
+ backgroundColor: theme.colors.primary,
319
+ borderRadius: responsive.scale(25),
320
+ paddingVertical: responsive.scale(18),
321
+ paddingHorizontal: getSpacing(32),
322
+ flexDirection: 'row',
323
+ alignItems: 'center',
324
+ justifyContent: 'space-between',
325
+ ...theme.shadows.button,
326
+ },
327
+ continueButtonDisabled: {
328
+ backgroundColor: theme.colors.textLight,
329
+ ...theme.shadows.small,
330
+ },
331
+ continueButtonText: {
332
+ fontSize: fontSize(16),
333
+ fontWeight: '600',
334
+ color: theme.colors.textInverse,
335
+ position: 'absolute',
336
+ left: 0,
337
+ right: 0,
338
+ textAlign: 'center',
339
+ },
340
+ buttonSpacer: {
341
+ width: responsive.scale(20),
342
+ },
343
+
344
+ // Footer content wrapper
345
+ footerContentWrapper: {
346
+ alignItems: 'center',
347
+ },
348
+
349
+ // Loading wrapper
350
+ loadingWrapper: {
351
+ flex: 1,
352
+ justifyContent: 'center',
353
+ alignItems: 'center',
354
+ },
355
+ });
356
+ };