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,311 @@
1
+ import React, { 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 { createTheme } from '@/constants/Theme';
15
+ import { responsive, fontSize, getSpacing } from '@/utils/responsive';
16
+
17
+ type ThemeType = 'light' | 'dark';
18
+
19
+ interface PaywallLayoutProps {
20
+ children?: React.ReactNode; // For the image/content area
21
+ title: string;
22
+ subtitle: string;
23
+ theme?: ThemeType;
24
+ onContinue: () => void;
25
+ continueText?: string;
26
+ footerContent?: React.ReactNode; // For billing info and footer links in paywall
27
+ continueDisabled?: boolean;
28
+ continueLoading?: boolean;
29
+ loadingContent?: React.ReactNode;
30
+ middleContent?: React.ReactNode; // For subscription plans in paywall
31
+ onBack?: () => void; // Back button functionality for in-app use
32
+ }
33
+
34
+ export default function PaywallLayout({
35
+ children,
36
+ title,
37
+ subtitle,
38
+ theme: themeProp = 'light',
39
+ onContinue,
40
+ continueText = 'Continue',
41
+ footerContent,
42
+ continueDisabled = false,
43
+ continueLoading = false,
44
+ loadingContent,
45
+ middleContent,
46
+ onBack,
47
+ }: PaywallLayoutProps) {
48
+ // Get reactive theme, but allow prop override for backwards compatibility
49
+ const contextTheme = useAppTheme();
50
+ const theme = themeProp === 'dark' ? createTheme('dark') : contextTheme;
51
+ const styles = useMemo(() => createStyles(theme), [theme]);
52
+
53
+ return (
54
+ <SafeAreaView style={styles.container}>
55
+ <KeyboardAvoidingView
56
+ style={styles.keyboardAvoidingView}
57
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
58
+ >
59
+ {/* Back Button */}
60
+ {onBack && (
61
+ <TouchableOpacity
62
+ style={styles.backButton}
63
+ onPress={onBack}
64
+ activeOpacity={0.7}
65
+ >
66
+ <IconSymbol
67
+ name="chevron.left"
68
+ size={responsive.moderateScale(24)}
69
+ color={theme.colors.icon}
70
+ />
71
+ </TouchableOpacity>
72
+ )}
73
+
74
+ <View style={styles.content}>
75
+ {loadingContent ? (
76
+ <View style={styles.loadingWrapper}>
77
+ {loadingContent}
78
+ </View>
79
+ ) : (
80
+ <>
81
+ {/* Scrollable content area */}
82
+ <ScrollView
83
+ style={styles.scrollView}
84
+ contentContainerStyle={styles.scrollContent}
85
+ showsVerticalScrollIndicator={false}
86
+ bounces={false}
87
+ >
88
+ {/* Top Section - Image/Content Area */}
89
+ <View style={styles.topSection}>
90
+ {children || (
91
+ <View style={styles.imagePlaceholder}>
92
+ {/* Default placeholder */}
93
+ </View>
94
+ )}
95
+ </View>
96
+
97
+ {/* Text Content */}
98
+ <View style={styles.textContainer}>
99
+ <Text style={styles.title}>{title}</Text>
100
+ <Text style={styles.subtitle}>{subtitle}</Text>
101
+ </View>
102
+
103
+ {/* Middle Content - Subscription Plans (only in paywall) */}
104
+ {middleContent && (
105
+ <View style={styles.middleContentWrapper}>
106
+ {middleContent}
107
+ </View>
108
+ )}
109
+
110
+ {/* Bottom padding to ensure content doesn't overlap with fixed bottom */}
111
+ <View style={styles.bottomPadding} />
112
+ </ScrollView>
113
+
114
+ {/* Fixed Bottom Content - Background and spacing */}
115
+ <View style={[
116
+ styles.fixedBottomContent,
117
+ footerContent ? styles.fixedBottomContentWithFooter : styles.fixedBottomContentNoFooter
118
+ ]}>
119
+ {/* Footer Content (billing info, links) - positioned at bottom */}
120
+ {footerContent && (
121
+ <View style={styles.footerContentWrapper}>
122
+ {footerContent}
123
+ </View>
124
+ )}
125
+ </View>
126
+
127
+ {/* Continue Button - Fixed position from bottom */}
128
+ <View style={styles.buttonContainer}>
129
+ <TouchableOpacity
130
+ style={[
131
+ styles.continueButton,
132
+ (continueDisabled || continueLoading) && styles.continueButtonDisabled
133
+ ]}
134
+ onPress={onContinue}
135
+ activeOpacity={continueDisabled || continueLoading ? 1 : 0.7}
136
+ disabled={continueDisabled || continueLoading}
137
+ >
138
+ <View style={styles.buttonSpacer} />
139
+ <Text style={styles.continueButtonText}>{continueText}</Text>
140
+ <IconSymbol
141
+ name="arrow.right"
142
+ size={20}
143
+ color={theme.colors.textInverse}
144
+ />
145
+ </TouchableOpacity>
146
+ </View>
147
+ </>
148
+ )}
149
+ </View>
150
+ </KeyboardAvoidingView>
151
+ </SafeAreaView>
152
+ );
153
+ }
154
+
155
+ const createStyles = (theme: AppTheme) => {
156
+ const sectionHeights = responsive.getSectionHeights();
157
+ const imageSize = responsive.getImageContainerSize();
158
+
159
+ return StyleSheet.create({
160
+ container: {
161
+ flex: 1,
162
+ backgroundColor: theme.colors.background,
163
+ },
164
+ keyboardAvoidingView: {
165
+ flex: 1,
166
+ },
167
+ backButton: {
168
+ position: 'absolute',
169
+ top: responsive.verticalScale(60),
170
+ left: getSpacing(24),
171
+ width: responsive.scale(44),
172
+ height: responsive.scale(44),
173
+ backgroundColor: theme.colors.backgroundSecondary,
174
+ borderRadius: responsive.scale(22),
175
+ justifyContent: 'center',
176
+ alignItems: 'center',
177
+ zIndex: 10,
178
+ ...theme.shadows.small,
179
+ },
180
+ content: {
181
+ flex: 1,
182
+ paddingHorizontal: getSpacing(24),
183
+ },
184
+
185
+ // ScrollView styles
186
+ scrollView: {
187
+ flex: 1,
188
+ },
189
+ scrollContent: {
190
+ flexGrow: 1,
191
+ paddingTop: getSpacing(16),
192
+ },
193
+
194
+ // Top Section - Image area
195
+ topSection: {
196
+ height: sectionHeights.topSection,
197
+ justifyContent: 'center',
198
+ alignItems: 'center',
199
+ minHeight: responsive.verticalScale(200),
200
+ },
201
+
202
+ // Image placeholder
203
+ imagePlaceholder: {
204
+ width: imageSize.width,
205
+ height: imageSize.height,
206
+ backgroundColor: theme.colors.backgroundTertiary,
207
+ borderRadius: responsive.scale(20),
208
+ borderWidth: 2,
209
+ borderColor: theme.colors.borderLight,
210
+ borderStyle: 'dashed',
211
+ },
212
+
213
+ // Text content
214
+ textContainer: {
215
+ alignItems: 'center',
216
+ paddingHorizontal: responsive.scale(20),
217
+ marginBottom: responsive.scale(16),
218
+ },
219
+ title: {
220
+ fontSize: fontSize(24),
221
+ fontWeight: '700',
222
+ color: theme.colors.text,
223
+ textAlign: 'center',
224
+ lineHeight: fontSize(30),
225
+ marginTop: responsive.scale(-12),
226
+ marginBottom: responsive.scale(8),
227
+ },
228
+ subtitle: {
229
+ fontSize: fontSize(16),
230
+ color: theme.colors.textSecondary,
231
+ textAlign: 'center',
232
+ lineHeight: fontSize(22),
233
+ },
234
+
235
+ // Middle content wrapper (for subscription plans)
236
+ middleContentWrapper: {
237
+ marginTop: getSpacing(16),
238
+ marginBottom: getSpacing(12),
239
+ flex: 1,
240
+ justifyContent: 'center',
241
+ },
242
+
243
+ // Bottom padding for ScrollView to prevent content overlap with fixed bottom
244
+ bottomPadding: {
245
+ height: responsive.verticalScale(140),
246
+ },
247
+
248
+ // Fixed bottom content container
249
+ fixedBottomContent: {
250
+ position: 'absolute',
251
+ bottom: 0,
252
+ left: 0,
253
+ right: 0,
254
+ backgroundColor: theme.colors.background,
255
+ paddingHorizontal: getSpacing(24),
256
+ paddingTop: getSpacing(20),
257
+ },
258
+ fixedBottomContentWithFooter: {
259
+ paddingBottom: 0,
260
+ },
261
+ fixedBottomContentNoFooter: {
262
+ paddingBottom: getSpacing(20),
263
+ },
264
+
265
+ // Continue button - Fixed position from bottom
266
+ buttonContainer: {
267
+ position: 'absolute',
268
+ bottom: responsive.getButtonBottomPosition(),
269
+ left: getSpacing(24),
270
+ right: getSpacing(24),
271
+ },
272
+ continueButton: {
273
+ backgroundColor: theme.colors.primary,
274
+ borderRadius: responsive.scale(25),
275
+ paddingVertical: responsive.scale(18),
276
+ paddingHorizontal: getSpacing(32),
277
+ flexDirection: 'row',
278
+ alignItems: 'center',
279
+ justifyContent: 'space-between',
280
+ ...theme.shadows.button,
281
+ },
282
+ continueButtonDisabled: {
283
+ backgroundColor: theme.colors.textLight,
284
+ ...theme.shadows.small,
285
+ },
286
+ continueButtonText: {
287
+ fontSize: fontSize(16),
288
+ fontWeight: '600',
289
+ color: theme.colors.textInverse,
290
+ position: 'absolute',
291
+ left: 0,
292
+ right: 0,
293
+ textAlign: 'center',
294
+ },
295
+ buttonSpacer: {
296
+ width: responsive.scale(20),
297
+ },
298
+
299
+ // Footer content wrapper
300
+ footerContentWrapper: {
301
+ alignItems: 'center',
302
+ },
303
+
304
+ // Loading wrapper
305
+ loadingWrapper: {
306
+ flex: 1,
307
+ justifyContent: 'center',
308
+ alignItems: 'center',
309
+ },
310
+ });
311
+ };
@@ -0,0 +1,58 @@
1
+ import React, { useEffect, useRef, useMemo } from 'react';
2
+ import { View, StyleSheet, Animated, ViewStyle } from 'react-native';
3
+ import { useAppTheme, AppTheme } from '@/context/ThemeContext';
4
+
5
+ interface SkeletonProps {
6
+ width?: number | string;
7
+ height?: number;
8
+ borderRadius?: number;
9
+ style?: ViewStyle;
10
+ }
11
+
12
+ export const Skeleton: React.FC<SkeletonProps> = ({
13
+ width = '100%', height = 20, borderRadius, style,
14
+ }) => {
15
+ const theme = useAppTheme();
16
+ const styles = useMemo(() => createStyles(theme), [theme]);
17
+ const shimmerAnim = useRef(new Animated.Value(0)).current;
18
+
19
+ const resolvedBorderRadius = borderRadius ?? theme.borderRadius.md;
20
+
21
+ useEffect(() => {
22
+ const animation = Animated.loop(
23
+ Animated.sequence([
24
+ Animated.timing(shimmerAnim, { toValue: 1, duration: 1000, useNativeDriver: true }),
25
+ Animated.timing(shimmerAnim, { toValue: 0, duration: 1000, useNativeDriver: true }),
26
+ ])
27
+ );
28
+ animation.start();
29
+ return () => animation.stop();
30
+ }, [shimmerAnim]);
31
+
32
+ const opacity = shimmerAnim.interpolate({ inputRange: [0, 1], outputRange: [0.3, 0.7] });
33
+
34
+ return (
35
+ <Animated.View style={[styles.skeleton, { width, height, borderRadius: resolvedBorderRadius, opacity }, style]} />
36
+ );
37
+ };
38
+
39
+ export const SkeletonText: React.FC<{ lines?: number; style?: ViewStyle }> = ({ lines = 3, style }) => {
40
+ const theme = useAppTheme();
41
+
42
+ return (
43
+ <View style={style}>
44
+ {Array.from({ length: lines }).map((_, i) => (
45
+ <Skeleton
46
+ key={i}
47
+ height={16}
48
+ width={i === lines - 1 ? '60%' : '100%'}
49
+ style={i < lines - 1 ? { marginBottom: theme.spacing[2] } : undefined}
50
+ />
51
+ ))}
52
+ </View>
53
+ );
54
+ };
55
+
56
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
57
+ skeleton: { backgroundColor: theme.colors.backgroundTertiary },
58
+ });
@@ -0,0 +1,6 @@
1
+ // Export all UI components
2
+ export * from './Button';
3
+ export * from './Input';
4
+ export * from './LoadingSpinner';
5
+ export * from './Card';
6
+ export * from './Skeleton';
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Design system for Mobile Platform
3
+ * Unified theme with light/dark mode support
4
+ */
5
+
6
+ import { Platform } from 'react-native';
7
+
8
+ // Shared palette
9
+ const palette = {
10
+ neutral: {
11
+ 0: '#ffffff',
12
+ 50: '#fafafa',
13
+ 100: '#f4f4f5',
14
+ 200: '#e4e4e7',
15
+ 300: '#d4d4d8',
16
+ 400: '#a1a1aa',
17
+ 500: '#71717a',
18
+ 600: '#52525b',
19
+ 700: '#3f3f46',
20
+ 800: '#27272a',
21
+ 900: '#18181b',
22
+ 950: '#09090b',
23
+ },
24
+ semantic: {
25
+ success: '#22c55e',
26
+ warning: '#f59e0b',
27
+ error: '#ef4444',
28
+ info: '#3b82f6',
29
+ },
30
+ };
31
+
32
+ const spacing = {
33
+ 1: 4, 2: 8, 3: 12, 4: 16,
34
+ 5: 20, 6: 24, 8: 32, 10: 40,
35
+ 12: 48, 16: 64, 20: 80, 24: 96,
36
+ };
37
+
38
+ const borderRadius = {
39
+ none: 0, sm: 4, md: 8, lg: 12, xl: 16, '2xl': 24, full: 9999,
40
+ };
41
+
42
+ const fontSize = {
43
+ xs: 12, sm: 14, base: 16, lg: 18,
44
+ xl: 20, '2xl': 24, '3xl': 30, '4xl': 36,
45
+ };
46
+
47
+ const fontFamily = Platform.OS === 'ios'
48
+ ? { regular: 'System', medium: 'System', semiBold: 'System', bold: 'System' }
49
+ : { regular: 'Roboto', medium: 'Roboto-Medium', semiBold: 'Roboto-Medium', bold: 'Roboto-Bold' };
50
+
51
+ // Light theme colors (monochrome primary matching web)
52
+ const lightColors = {
53
+ background: palette.neutral[0],
54
+ backgroundSecondary: palette.neutral[50],
55
+ backgroundTertiary: palette.neutral[100],
56
+ card: palette.neutral[0],
57
+
58
+ text: palette.neutral[950],
59
+ textSecondary: palette.neutral[600],
60
+ textMuted: palette.neutral[500],
61
+ textLight: palette.neutral[400],
62
+ textInverse: palette.neutral[0],
63
+
64
+ border: palette.neutral[200],
65
+ borderLight: palette.neutral[100],
66
+ borderStrong: palette.neutral[400],
67
+
68
+ // Monochrome primary (matches web design system)
69
+ primary: palette.neutral[900],
70
+ primaryDark: palette.neutral[950],
71
+ primaryLight: palette.neutral[100],
72
+
73
+ icon: palette.neutral[700],
74
+ iconMuted: palette.neutral[400],
75
+
76
+ success: palette.semantic.success,
77
+ warning: palette.semantic.warning,
78
+ error: palette.semantic.error,
79
+ info: palette.semantic.info,
80
+ };
81
+
82
+ // Dark theme colors (monochrome primary matching web)
83
+ const darkColors = {
84
+ background: palette.neutral[950],
85
+ backgroundSecondary: palette.neutral[900],
86
+ backgroundTertiary: palette.neutral[800],
87
+ card: palette.neutral[900],
88
+
89
+ text: palette.neutral[50],
90
+ textSecondary: palette.neutral[400],
91
+ textMuted: palette.neutral[500],
92
+ textLight: palette.neutral[600],
93
+ textInverse: palette.neutral[950],
94
+
95
+ border: palette.neutral[800],
96
+ borderLight: palette.neutral[900],
97
+ borderStrong: palette.neutral[500],
98
+
99
+ // Monochrome primary (matches web design system)
100
+ primary: palette.neutral[50],
101
+ primaryDark: palette.neutral[0],
102
+ primaryLight: palette.neutral[800],
103
+
104
+ icon: palette.neutral[300],
105
+ iconMuted: palette.neutral[500],
106
+
107
+ success: '#4ade80',
108
+ warning: '#fbbf24',
109
+ error: '#f87171',
110
+ info: '#60a5fa',
111
+ };
112
+
113
+ const createShadows = (mode: 'light' | 'dark') => {
114
+ const opacity = mode === 'dark' ? 0.4 : 0.08;
115
+ return {
116
+ none: {},
117
+ small: {
118
+ shadowColor: '#000',
119
+ shadowOffset: { width: 0, height: 1 },
120
+ shadowOpacity: opacity * 0.5,
121
+ shadowRadius: 2,
122
+ elevation: 1,
123
+ },
124
+ medium: {
125
+ shadowColor: '#000',
126
+ shadowOffset: { width: 0, height: 2 },
127
+ shadowOpacity: opacity,
128
+ shadowRadius: 4,
129
+ elevation: 3,
130
+ },
131
+ large: {
132
+ shadowColor: '#000',
133
+ shadowOffset: { width: 0, height: 4 },
134
+ shadowOpacity: opacity * 1.5,
135
+ shadowRadius: 8,
136
+ elevation: 8,
137
+ },
138
+ button: {
139
+ shadowColor: palette.neutral[900],
140
+ shadowOffset: { width: 0, height: 2 },
141
+ shadowOpacity: 0.15,
142
+ shadowRadius: 4,
143
+ elevation: 3,
144
+ },
145
+ };
146
+ };
147
+
148
+ export type ThemeMode = 'light' | 'dark';
149
+
150
+ export const createTheme = (mode: ThemeMode = 'light') => ({
151
+ mode,
152
+ colors: mode === 'light' ? lightColors : darkColors,
153
+ palette,
154
+ spacing,
155
+ borderRadius,
156
+ typography: { fontFamily, fontSize },
157
+ shadows: createShadows(mode),
158
+ timing: { fast: 150, base: 200, slow: 300 },
159
+ });
160
+
161
+ // Default export for backwards compatibility
162
+ export const Theme = createTheme('light');
163
+ export type AppTheme = ReturnType<typeof createTheme>;
@@ -0,0 +1,157 @@
1
+ import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
2
+ import { useColorScheme, Appearance } from 'react-native';
3
+ import AsyncStorage from '@react-native-async-storage/async-storage';
4
+ import { StatusBar } from 'expo-status-bar';
5
+ import { createTheme, ThemeMode, AppTheme } from '@/constants/Theme';
6
+
7
+ const THEME_STORAGE_KEY = '@app_theme_mode';
8
+
9
+ interface ThemeContextType {
10
+ theme: AppTheme;
11
+ mode: ThemeMode;
12
+ setMode: (mode: ThemeMode) => void;
13
+ toggleMode: () => void;
14
+ isSystemDefault: boolean;
15
+ setSystemDefault: (useSystem: boolean) => void;
16
+ }
17
+
18
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
19
+
20
+ interface ThemeProviderProps {
21
+ children: React.ReactNode;
22
+ }
23
+
24
+ export function ThemeProvider({ children }: ThemeProviderProps) {
25
+ const systemColorScheme = useColorScheme();
26
+ // Initialize with system theme using Appearance API directly (more reliable than hook)
27
+ const [mode, setModeState] = useState<ThemeMode>(
28
+ () => Appearance.getColorScheme() || 'light'
29
+ );
30
+ const [isSystemDefault, setIsSystemDefault] = useState(true);
31
+
32
+ // Load saved theme preference
33
+ useEffect(() => {
34
+ (async () => {
35
+ try {
36
+ const saved = await AsyncStorage.getItem(THEME_STORAGE_KEY);
37
+ const currentSystemTheme = Appearance.getColorScheme();
38
+
39
+ if (saved) {
40
+ const { mode: savedMode, useSystem } = JSON.parse(saved);
41
+ setIsSystemDefault(useSystem);
42
+ if (!useSystem) {
43
+ setModeState(savedMode);
44
+ } else if (currentSystemTheme) {
45
+ setModeState(currentSystemTheme);
46
+ }
47
+ } else if (currentSystemTheme) {
48
+ setModeState(currentSystemTheme);
49
+ }
50
+ } catch {
51
+ // Use defaults on error
52
+ }
53
+ })();
54
+ }, []);
55
+
56
+ // Sync with system preference when using system default
57
+ useEffect(() => {
58
+ if (isSystemDefault && systemColorScheme) {
59
+ setModeState(systemColorScheme);
60
+ }
61
+ }, [isSystemDefault, systemColorScheme]);
62
+
63
+ // Listen for real-time system theme changes
64
+ useEffect(() => {
65
+ const subscription = Appearance.addChangeListener(({ colorScheme }) => {
66
+ if (isSystemDefault && colorScheme) {
67
+ setModeState(colorScheme);
68
+ }
69
+ });
70
+
71
+ return () => subscription.remove();
72
+ }, [isSystemDefault]);
73
+
74
+ const setMode = useCallback(async (newMode: ThemeMode) => {
75
+ setModeState(newMode);
76
+ setIsSystemDefault(false);
77
+ try {
78
+ await AsyncStorage.setItem(
79
+ THEME_STORAGE_KEY,
80
+ JSON.stringify({ mode: newMode, useSystem: false })
81
+ );
82
+ } catch {
83
+ // Ignore storage errors
84
+ }
85
+ }, []);
86
+
87
+ const toggleMode = useCallback(() => {
88
+ setModeState((current) => {
89
+ const newMode = current === 'light' ? 'dark' : 'light';
90
+ AsyncStorage.setItem(
91
+ THEME_STORAGE_KEY,
92
+ JSON.stringify({ mode: newMode, useSystem: false })
93
+ ).catch(() => {});
94
+ return newMode;
95
+ });
96
+ setIsSystemDefault(false);
97
+ }, []);
98
+
99
+ const handleSetSystemDefault = useCallback(async (useSystem: boolean) => {
100
+ setIsSystemDefault(useSystem);
101
+
102
+ // Determine the mode to use and store
103
+ let modeToStore = mode;
104
+ if (useSystem && systemColorScheme) {
105
+ modeToStore = systemColorScheme;
106
+ setModeState(modeToStore);
107
+ }
108
+
109
+ try {
110
+ await AsyncStorage.setItem(
111
+ THEME_STORAGE_KEY,
112
+ JSON.stringify({ mode: modeToStore, useSystem })
113
+ );
114
+ } catch {
115
+ // Ignore storage errors
116
+ }
117
+ }, [systemColorScheme, mode]);
118
+
119
+ const theme = useMemo(() => createTheme(mode), [mode]);
120
+
121
+ const value = useMemo(
122
+ () => ({
123
+ theme,
124
+ mode,
125
+ setMode,
126
+ toggleMode,
127
+ isSystemDefault,
128
+ setSystemDefault: handleSetSystemDefault,
129
+ }),
130
+ [theme, mode, setMode, toggleMode, isSystemDefault, handleSetSystemDefault]
131
+ );
132
+
133
+ return (
134
+ <ThemeContext.Provider value={value}>
135
+ <StatusBar style={mode === 'dark' ? 'light' : 'dark'} />
136
+ {children}
137
+ </ThemeContext.Provider>
138
+ );
139
+ }
140
+
141
+ export function useTheme() {
142
+ const context = useContext(ThemeContext);
143
+ if (!context) {
144
+ throw new Error('useTheme must be used within a ThemeProvider');
145
+ }
146
+ return context;
147
+ }
148
+
149
+ // Convenience hook for just the theme object (backwards compatible)
150
+ export function useAppTheme(): AppTheme {
151
+ const context = useContext(ThemeContext);
152
+ // Fallback to default light theme if no provider
153
+ return context?.theme ?? createTheme('light');
154
+ }
155
+
156
+ // Re-export AppTheme for convenience (so components can import from one place)
157
+ export type { AppTheme } from '@/constants/Theme';