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,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
@@ -0,0 +1,16 @@
1
+ import { Stack } from 'expo-router';
2
+
3
+ export default function AuthLayout() {
4
+ return (
5
+ <Stack
6
+ screenOptions={{
7
+ headerShown: false,
8
+ presentation: 'modal',
9
+ animation: 'slide_from_bottom',
10
+ }}
11
+ >
12
+ <Stack.Screen name="login" />
13
+ <Stack.Screen name="register" />
14
+ </Stack>
15
+ );
16
+ }
@@ -0,0 +1,86 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { View, StyleSheet, SafeAreaView, TouchableOpacity } from 'react-native';
3
+ import { router, Stack } from 'expo-router';
4
+ import { LoginForm } from '../../src/components/auth';
5
+ import { IconSymbol } from '../../src/components/ui/IconSymbol';
6
+ import { useAppTheme, AppTheme } from '@/context/ThemeContext';
7
+ import { useAuth } from '../../src/hooks';
8
+
9
+ export default function LoginScreen() {
10
+ const theme = useAppTheme();
11
+ const styles = useMemo(() => createStyles(theme), [theme]);
12
+
13
+ const { isAuthenticated } = useAuth();
14
+
15
+ // Watch for auth state changes (handles both native and browser OAuth)
16
+ useEffect(() => {
17
+ if (isAuthenticated) {
18
+ router.replace('/(tabs)');
19
+ }
20
+ }, [isAuthenticated]);
21
+
22
+ const handleLoginSuccess = () => {
23
+ // Navigate to the main app (for email/password login)
24
+ router.replace('/(tabs)');
25
+ };
26
+
27
+ const handleSwitchToRegister = () => {
28
+ router.push('/(auth)/register');
29
+ };
30
+
31
+ const handleBack = () => {
32
+ if (router.canGoBack()) {
33
+ router.back();
34
+ }
35
+ };
36
+
37
+ return (
38
+ <>
39
+ <Stack.Screen options={{ headerShown: false }} />
40
+ <SafeAreaView style={styles.container}>
41
+ {/* Back Button */}
42
+ <TouchableOpacity
43
+ style={styles.backButton}
44
+ onPress={handleBack}
45
+ activeOpacity={0.7}
46
+ >
47
+ <IconSymbol
48
+ name="chevron.left"
49
+ size={24}
50
+ color={theme.colors.text}
51
+ />
52
+ </TouchableOpacity>
53
+
54
+ <View style={styles.content}>
55
+ <LoginForm
56
+ onSuccess={handleLoginSuccess}
57
+ onSwitchToRegister={handleSwitchToRegister}
58
+ />
59
+ </View>
60
+ </SafeAreaView>
61
+ </>
62
+ );
63
+ }
64
+
65
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
66
+ container: {
67
+ flex: 1,
68
+ backgroundColor: theme.colors.background,
69
+ },
70
+ backButton: {
71
+ position: 'absolute',
72
+ top: 60,
73
+ left: 16,
74
+ width: 44,
75
+ height: 44,
76
+ backgroundColor: theme.colors.backgroundSecondary,
77
+ borderRadius: 22,
78
+ justifyContent: 'center',
79
+ alignItems: 'center',
80
+ zIndex: 10,
81
+ ...theme.shadows.small,
82
+ },
83
+ content: {
84
+ flex: 1,
85
+ },
86
+ });
@@ -0,0 +1,86 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { View, StyleSheet, SafeAreaView, TouchableOpacity } from 'react-native';
3
+ import { router, Stack } from 'expo-router';
4
+ import { RegisterForm } from '../../src/components/auth';
5
+ import { IconSymbol } from '../../src/components/ui/IconSymbol';
6
+ import { useAppTheme, AppTheme } from '@/context/ThemeContext';
7
+ import { useAuth } from '../../src/hooks';
8
+
9
+ export default function RegisterScreen() {
10
+ const theme = useAppTheme();
11
+ const styles = useMemo(() => createStyles(theme), [theme]);
12
+
13
+ const { isAuthenticated } = useAuth();
14
+
15
+ // Watch for auth state changes (handles both native and browser OAuth)
16
+ useEffect(() => {
17
+ if (isAuthenticated) {
18
+ router.replace('/(tabs)');
19
+ }
20
+ }, [isAuthenticated]);
21
+
22
+ const handleRegisterSuccess = () => {
23
+ // Navigate to the main app (for email/password registration)
24
+ router.replace('/(tabs)');
25
+ };
26
+
27
+ const handleSwitchToLogin = () => {
28
+ router.back();
29
+ };
30
+
31
+ const handleBack = () => {
32
+ if (router.canGoBack()) {
33
+ router.back();
34
+ }
35
+ };
36
+
37
+ return (
38
+ <>
39
+ <Stack.Screen options={{ headerShown: false }} />
40
+ <SafeAreaView style={styles.container}>
41
+ {/* Back Button */}
42
+ <TouchableOpacity
43
+ style={styles.backButton}
44
+ onPress={handleBack}
45
+ activeOpacity={0.7}
46
+ >
47
+ <IconSymbol
48
+ name="chevron.left"
49
+ size={24}
50
+ color={theme.colors.text}
51
+ />
52
+ </TouchableOpacity>
53
+
54
+ <View style={styles.content}>
55
+ <RegisterForm
56
+ onSuccess={handleRegisterSuccess}
57
+ onSwitchToLogin={handleSwitchToLogin}
58
+ />
59
+ </View>
60
+ </SafeAreaView>
61
+ </>
62
+ );
63
+ }
64
+
65
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
66
+ container: {
67
+ flex: 1,
68
+ backgroundColor: theme.colors.background,
69
+ },
70
+ backButton: {
71
+ position: 'absolute',
72
+ top: 60,
73
+ left: 16,
74
+ width: 44,
75
+ height: 44,
76
+ backgroundColor: theme.colors.backgroundSecondary,
77
+ borderRadius: 22,
78
+ justifyContent: 'center',
79
+ alignItems: 'center',
80
+ zIndex: 10,
81
+ ...theme.shadows.small,
82
+ },
83
+ content: {
84
+ flex: 1,
85
+ },
86
+ });
@@ -0,0 +1,349 @@
1
+ <% const hasOAuth = features.authentication.providers.google || features.authentication.providers.apple || features.authentication.providers.github; %>
2
+ import React, { useState, useEffect, useMemo } from 'react';
3
+ import {
4
+ View,
5
+ Text,
6
+ StyleSheet,
7
+ KeyboardAvoidingView,
8
+ Platform,
9
+ ScrollView,
10
+ Alert,
11
+ <% if (hasOAuth) { %>
12
+ TouchableOpacity,
13
+ <% } %>
14
+ } from 'react-native';
15
+ import { Button, Input } from '../ui';
16
+ import { useAuth } from '../../hooks';
17
+ import { isValidEmail } from '../../utils/formatters';
18
+ import { useAppTheme, AppTheme } from '@/context/ThemeContext';
19
+
20
+ interface LoginFormProps {
21
+ onSuccess?: () => void;
22
+ onSwitchToRegister?: () => void;
23
+ }
24
+
25
+ export const LoginForm: React.FC<LoginFormProps> = ({
26
+ onSuccess,
27
+ onSwitchToRegister,
28
+ }) => {
29
+ const theme = useAppTheme();
30
+ const styles = useMemo(() => createStyles(theme), [theme]);
31
+
32
+ const [email, setEmail] = useState('');
33
+ const [password, setPassword] = useState('');
34
+ const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
35
+
36
+ const {
37
+ signIn,
38
+ isLoading,
39
+ clearError,
40
+ <% if (features.authentication.providers.google) { %>
41
+ signInWithGoogle,
42
+ <% } %>
43
+ <% if (features.authentication.providers.apple) { %>
44
+ signInWithApple,
45
+ <% } %>
46
+ <% if (features.authentication.providers.github) { %>
47
+ signInWithGitHub,
48
+ <% } %>
49
+ } = useAuth();
50
+
51
+ // Clear auth error on unmount
52
+ useEffect(() => {
53
+ return () => clearError();
54
+ }, [clearError]);
55
+
56
+ const validateForm = (): boolean => {
57
+ const newErrors: { email?: string; password?: string } = {};
58
+
59
+ if (!email.trim()) {
60
+ newErrors.email = 'Email is required';
61
+ } else if (!isValidEmail(email.trim())) {
62
+ newErrors.email = 'Please enter a valid email';
63
+ }
64
+
65
+ if (!password.trim()) {
66
+ newErrors.password = 'Password is required';
67
+ } else if (password.length < 6) {
68
+ newErrors.password = 'Password must be at least 6 characters';
69
+ }
70
+
71
+ setErrors(newErrors);
72
+ return Object.keys(newErrors).length === 0;
73
+ };
74
+
75
+ const handleSubmit = async () => {
76
+ if (!validateForm()) return;
77
+
78
+ const result = await signIn({
79
+ email: email.trim().toLowerCase(),
80
+ password,
81
+ });
82
+
83
+ if (result.success) {
84
+ onSuccess?.();
85
+ } else {
86
+ // Show server error via Alert
87
+ Alert.alert('Sign In Failed', result.error || 'Please check your credentials and try again.');
88
+ }
89
+ };
90
+ <% if (features.authentication.providers.google) { %>
91
+
92
+ const handleGoogleSignIn = async () => {
93
+ const result = await signInWithGoogle();
94
+ // Don't call onSuccess here - browser OAuth completes asynchronously
95
+ // via deep link callback. The parent screen watches isAuthenticated
96
+ // state and will navigate when the session is established.
97
+ if (result.error && result.error !== 'Sign in cancelled') {
98
+ Alert.alert('Sign In Failed', result.error);
99
+ }
100
+ };
101
+ <% } %>
102
+ <% if (features.authentication.providers.apple) { %>
103
+
104
+ const handleAppleSignIn = async () => {
105
+ const result = await signInWithApple();
106
+ // Don't call onSuccess here - browser OAuth completes asynchronously
107
+ if (result.error && result.error !== 'Sign in cancelled') {
108
+ Alert.alert('Sign In Failed', result.error);
109
+ }
110
+ };
111
+ <% } %>
112
+ <% if (features.authentication.providers.github) { %>
113
+
114
+ const handleGitHubSignIn = async () => {
115
+ const result = await signInWithGitHub();
116
+ // Don't call onSuccess here - browser OAuth completes asynchronously
117
+ if (result.error && result.error !== 'Sign in cancelled') {
118
+ Alert.alert('Sign In Failed', result.error);
119
+ }
120
+ };
121
+ <% } %>
122
+
123
+ return (
124
+ <KeyboardAvoidingView
125
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
126
+ style={styles.container}
127
+ >
128
+ <ScrollView
129
+ contentContainerStyle={styles.scrollContent}
130
+ keyboardShouldPersistTaps="handled"
131
+ >
132
+ <View style={styles.form}>
133
+ <Text style={styles.title}>Welcome Back</Text>
134
+ <Text style={styles.subtitle}>Sign in to your account</Text>
135
+
136
+ <Input
137
+ label="Email"
138
+ value={email}
139
+ onChangeText={(text) => {
140
+ setEmail(text);
141
+ if (errors.email) {
142
+ setErrors(prev => ({ ...prev, email: undefined }));
143
+ }
144
+ }}
145
+ placeholder="Enter your email"
146
+ keyboardType="email-address"
147
+ autoCapitalize="none"
148
+ autoComplete="email"
149
+ error={errors.email}
150
+ containerStyle={styles.inputContainer}
151
+ />
152
+
153
+ <Input
154
+ label="Password"
155
+ value={password}
156
+ onChangeText={(text) => {
157
+ setPassword(text);
158
+ if (errors.password) {
159
+ setErrors(prev => ({ ...prev, password: undefined }));
160
+ }
161
+ }}
162
+ placeholder="Enter your password"
163
+ secureTextEntry
164
+ showPasswordToggle
165
+ autoComplete="password"
166
+ error={errors.password}
167
+ containerStyle={styles.inputContainer}
168
+ />
169
+
170
+ <Button
171
+ title="Sign In"
172
+ onPress={handleSubmit}
173
+ loading={isLoading}
174
+ disabled={isLoading}
175
+ style={styles.submitButton}
176
+ />
177
+ <% if (hasOAuth) { %>
178
+
179
+ {/* OAuth Divider */}
180
+ <View style={styles.dividerContainer}>
181
+ <View style={styles.dividerLine} />
182
+ <Text style={styles.dividerText}>or continue with</Text>
183
+ <View style={styles.dividerLine} />
184
+ </View>
185
+
186
+ {/* OAuth Buttons */}
187
+ <View style={styles.oauthContainer}>
188
+ <% if (features.authentication.providers.google) { %>
189
+ <TouchableOpacity
190
+ style={styles.oauthButton}
191
+ onPress={handleGoogleSignIn}
192
+ disabled={isLoading}
193
+ activeOpacity={0.7}
194
+ >
195
+ <Text style={styles.oauthButtonText}>
196
+ <Text style={styles.googleIcon}>G</Text>{' '}Google
197
+ </Text>
198
+ </TouchableOpacity>
199
+ <% } %>
200
+ <% if (features.authentication.providers.apple) { %>
201
+ <TouchableOpacity
202
+ style={styles.oauthButton}
203
+ onPress={handleAppleSignIn}
204
+ disabled={isLoading}
205
+ activeOpacity={0.7}
206
+ >
207
+ <Text style={styles.oauthButtonText}>Apple</Text>
208
+ </TouchableOpacity>
209
+ <% } %>
210
+ <% if (features.authentication.providers.github) { %>
211
+ <TouchableOpacity
212
+ style={styles.oauthButton}
213
+ onPress={handleGitHubSignIn}
214
+ disabled={isLoading}
215
+ activeOpacity={0.7}
216
+ >
217
+ <Text style={styles.oauthButtonText}>GitHub</Text>
218
+ </TouchableOpacity>
219
+ <% } %>
220
+ </View>
221
+ <% } %>
222
+
223
+ {onSwitchToRegister && (
224
+ <View style={styles.switchContainer}>
225
+ <Text style={styles.switchText}>
226
+ Don't have an account?{' '}
227
+ </Text>
228
+ <Button
229
+ title="Sign Up"
230
+ variant="ghost"
231
+ onPress={onSwitchToRegister}
232
+ style={styles.switchButton}
233
+ textStyle={styles.switchButtonText}
234
+ />
235
+ </View>
236
+ )}
237
+ </View>
238
+ </ScrollView>
239
+ </KeyboardAvoidingView>
240
+ );
241
+ };
242
+
243
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
244
+ container: {
245
+ flex: 1,
246
+ },
247
+
248
+ scrollContent: {
249
+ flexGrow: 1,
250
+ justifyContent: 'center',
251
+ padding: theme.spacing[5],
252
+ },
253
+
254
+ form: {
255
+ width: '100%',
256
+ maxWidth: 400,
257
+ alignSelf: 'center',
258
+ },
259
+
260
+ title: {
261
+ fontSize: theme.typography.fontSize['2xl'],
262
+ fontWeight: 'bold',
263
+ color: theme.colors.text,
264
+ textAlign: 'center',
265
+ marginBottom: theme.spacing[2],
266
+ },
267
+
268
+ subtitle: {
269
+ fontSize: theme.typography.fontSize.base,
270
+ color: theme.colors.textSecondary,
271
+ textAlign: 'center',
272
+ marginBottom: theme.spacing[8],
273
+ },
274
+
275
+ inputContainer: {
276
+ marginBottom: theme.spacing[5],
277
+ },
278
+
279
+ submitButton: {
280
+ marginTop: theme.spacing[2],
281
+ marginBottom: theme.spacing[6],
282
+ },
283
+ <% if (hasOAuth) { %>
284
+
285
+ // Divider
286
+ dividerContainer: {
287
+ flexDirection: 'row',
288
+ alignItems: 'center',
289
+ marginBottom: theme.spacing[6],
290
+ },
291
+ dividerLine: {
292
+ flex: 1,
293
+ height: 1,
294
+ backgroundColor: theme.colors.border,
295
+ },
296
+ dividerText: {
297
+ marginHorizontal: theme.spacing[4],
298
+ fontSize: theme.typography.fontSize.sm,
299
+ color: theme.colors.textSecondary,
300
+ },
301
+
302
+ // OAuth buttons - theme-aware (matches web app pattern)
303
+ oauthContainer: {
304
+ gap: theme.spacing[3],
305
+ marginBottom: theme.spacing[6],
306
+ },
307
+ oauthButton: {
308
+ flexDirection: 'row',
309
+ alignItems: 'center',
310
+ justifyContent: 'center',
311
+ paddingVertical: 14,
312
+ borderRadius: theme.borderRadius.md,
313
+ borderWidth: 1,
314
+ backgroundColor: 'transparent',
315
+ borderColor: theme.colors.border,
316
+ },
317
+ oauthButtonText: {
318
+ fontSize: theme.typography.fontSize.base,
319
+ fontWeight: '600',
320
+ color: theme.colors.text,
321
+ },
322
+ googleIcon: {
323
+ color: theme.colors.primary,
324
+ fontWeight: '700',
325
+ fontSize: 18,
326
+ },
327
+ <% } %>
328
+
329
+ switchContainer: {
330
+ flexDirection: 'row',
331
+ justifyContent: 'center',
332
+ alignItems: 'center',
333
+ },
334
+
335
+ switchText: {
336
+ fontSize: theme.typography.fontSize.base,
337
+ color: theme.colors.textSecondary,
338
+ },
339
+
340
+ switchButton: {
341
+ paddingHorizontal: 0,
342
+ paddingVertical: 0,
343
+ minHeight: 'auto' as unknown as number,
344
+ },
345
+
346
+ switchButtonText: {
347
+ fontSize: theme.typography.fontSize.base,
348
+ },
349
+ });