create-stackr 0.2.0 → 0.3.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 (127) hide show
  1. package/README.md +10 -0
  2. package/dist/prompts/features.d.ts +1 -1
  3. package/dist/prompts/features.d.ts.map +1 -1
  4. package/dist/prompts/features.js +34 -25
  5. package/dist/prompts/features.js.map +1 -1
  6. package/dist/prompts/index.js +33 -6
  7. package/dist/prompts/index.js.map +1 -1
  8. package/dist/prompts/preset.d.ts.map +1 -1
  9. package/dist/prompts/preset.js +69 -34
  10. package/dist/prompts/preset.js.map +1 -1
  11. package/dist/utils/template.js +1 -1
  12. package/dist/utils/template.js.map +1 -1
  13. package/dist/utils/validation.d.ts.map +1 -1
  14. package/dist/utils/validation.js +43 -1
  15. package/dist/utils/validation.js.map +1 -1
  16. package/package.json +1 -1
  17. package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +33 -1
  18. package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +6 -0
  19. package/templates/base/backend/domain/user/repository.drizzle.ts +28 -1
  20. package/templates/base/backend/domain/user/repository.prisma.ts +25 -0
  21. package/templates/base/backend/drizzle/{schema.drizzle.ts → schema.drizzle.ts.ejs} +25 -0
  22. package/templates/base/backend/lib/auth.drizzle.ts.ejs +27 -18
  23. package/templates/base/backend/lib/auth.prisma.ts.ejs +24 -18
  24. package/templates/base/backend/package.json.ejs +29 -23
  25. package/templates/base/backend/prisma/schema.prisma.ejs +20 -0
  26. package/templates/base/mobile/app/+not-found.tsx +1 -1
  27. package/templates/base/mobile/app/_layout.tsx.ejs +95 -10
  28. package/templates/base/mobile/package.json.ejs +21 -13
  29. package/templates/base/mobile/src/components/ui/Button.tsx +5 -3
  30. package/templates/base/mobile/src/components/ui/Card.tsx +1 -1
  31. package/templates/base/mobile/src/components/ui/Input.tsx +1 -1
  32. package/templates/base/mobile/src/components/ui/Skeleton.tsx +1 -1
  33. package/templates/base/mobile/src/components/ui/Toast.tsx +106 -0
  34. package/templates/base/mobile/src/components/ui/{IconSymbol.tsx → icon-symbol.tsx} +7 -0
  35. package/templates/base/mobile/src/components/ui/{LoadingSpinner.tsx → loading-spinner.tsx} +1 -1
  36. package/templates/base/mobile/src/components/ui/{OnboardingLayout.tsx → onboarding-layout.tsx} +2 -2
  37. package/templates/base/mobile/src/components/ui/{PaywallLayout.tsx → paywall-layout.tsx} +3 -3
  38. package/templates/base/mobile/src/constants/Theme.ts +3 -3
  39. package/templates/base/mobile/src/context/{ThemeContext.tsx → theme-context.tsx} +2 -2
  40. package/templates/base/mobile/src/lib/auth-client.ts.ejs +18 -0
  41. package/templates/base/mobile/src/services/{sdkInitializer.ts.ejs → sdk-initializer.ts.ejs} +4 -4
  42. package/templates/base/web/.prettierignore +6 -0
  43. package/templates/base/web/.prettierrc +8 -0
  44. package/templates/base/web/eslint.config.mjs +31 -7
  45. package/templates/base/web/next.config.ts +50 -1
  46. package/templates/base/web/package.json.ejs +14 -2
  47. package/templates/base/web/src/app/globals.css +1 -1
  48. package/templates/base/web/src/app/layout.tsx.ejs +2 -0
  49. package/templates/base/web/src/components/auth/protected-route.tsx.ejs +32 -5
  50. package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +1 -1
  51. package/templates/base/web/src/hooks/use-device-session.ts.ejs +2 -2
  52. package/templates/base/web/src/lib/auth/actions.ts.ejs +438 -15
  53. package/templates/base/web/src/lib/auth/config.ts.ejs +13 -0
  54. package/templates/base/web/src/lib/auth/cookies.ts.ejs +61 -0
  55. package/templates/base/web/src/lib/device/actions.ts.ejs +2 -10
  56. package/templates/base/web/src/lib/device/types.ts +37 -0
  57. package/templates/base/web/src/proxy.ts.ejs +12 -2
  58. package/templates/base/web/src/store/{deviceSession.store.ts.ejs → device-session-store.ts.ejs} +1 -1
  59. package/templates/features/mobile/auth/app/(auth)/{_layout.tsx → _layout.tsx.ejs} +7 -0
  60. package/templates/features/mobile/auth/app/(auth)/{login.tsx → login.tsx.ejs} +9 -8
  61. package/templates/features/mobile/auth/app/(auth)/{register.tsx → register.tsx.ejs} +9 -8
  62. package/templates/features/mobile/auth/app/(auth)/two-factor-expired.tsx.ejs +88 -0
  63. package/templates/features/mobile/auth/app/(auth)/two-factor.tsx.ejs +259 -0
  64. package/templates/features/mobile/auth/app/(public)/_layout.tsx +9 -0
  65. package/templates/features/mobile/{tabs/app/(tabs)/index.tsx → auth/app/(public)/index.tsx.ejs} +76 -257
  66. package/templates/features/mobile/auth/app/(tabs)/settings/_layout.tsx.ejs +20 -0
  67. package/templates/features/mobile/auth/app/(tabs)/settings/index.tsx.ejs +137 -0
  68. package/templates/features/mobile/auth/app/(tabs)/settings/security/_layout.tsx.ejs +35 -0
  69. package/templates/features/mobile/auth/app/(tabs)/settings/security/index.tsx.ejs +147 -0
  70. package/templates/features/mobile/auth/app/(tabs)/settings/security/set-password.tsx.ejs +140 -0
  71. package/templates/features/mobile/auth/app/(tabs)/settings/security/two-factor-manage.tsx.ejs +366 -0
  72. package/templates/features/mobile/auth/app/(tabs)/settings/security/two-factor-setup.tsx.ejs +317 -0
  73. package/templates/features/mobile/auth/app/account.tsx.ejs +331 -0
  74. package/templates/features/mobile/auth/app/verify-email.tsx.ejs +315 -0
  75. package/templates/features/mobile/auth/components/auth/{LoginForm.tsx.ejs → login-form.tsx.ejs} +15 -3
  76. package/templates/features/mobile/auth/components/auth/protected-screen.tsx.ejs +56 -0
  77. package/templates/features/mobile/auth/components/auth/{RegisterForm.tsx.ejs → register-form.tsx.ejs} +5 -3
  78. package/templates/features/mobile/auth/hooks/{useAuth.ts.ejs → auth.ts.ejs} +255 -1
  79. package/templates/features/mobile/auth/hooks/two-factor-timeout.ts.ejs +56 -0
  80. package/templates/features/mobile/auth/services/{deviceSession.ts → device-session.ts} +2 -10
  81. package/templates/features/mobile/auth/store/{deviceSession.store.ts → device-session-store.ts} +14 -5
  82. package/templates/features/mobile/auth/types/device-session.ts +37 -0
  83. package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +3 -1
  84. package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +3 -1
  85. package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +6 -1
  86. package/templates/features/mobile/paywall/app/paywall.tsx +4 -4
  87. package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +0 -6
  88. package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx.ejs +60 -0
  89. package/templates/features/mobile/tabs/app/(tabs)/index.tsx.ejs +264 -0
  90. package/templates/features/web/auth/app/(app)/layout.tsx.ejs +8 -22
  91. package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +19 -3
  92. package/templates/features/web/auth/app/(auth)/login/two-factor/page.tsx.ejs +24 -0
  93. package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +117 -152
  94. package/templates/features/web/auth/app/{(app) → (protected)}/dashboard/dashboard-client.tsx.ejs +41 -1
  95. package/templates/features/web/auth/app/{(app) → (protected)}/dashboard/page.tsx.ejs +3 -1
  96. package/templates/features/web/auth/app/(protected)/layout.tsx.ejs +67 -0
  97. package/templates/features/web/auth/app/(protected)/settings/security/page.tsx.ejs +19 -0
  98. package/templates/features/web/auth/app/(protected)/settings/security/security-settings.tsx.ejs +73 -0
  99. package/templates/features/web/auth/app/{(app) → (protected)}/settings/sessions/page.tsx.ejs +2 -6
  100. package/templates/features/web/auth/app/auth/callback/route.ts.ejs +5 -1
  101. package/templates/features/web/auth/app/auth/session-expired/route.ts.ejs +39 -0
  102. package/templates/features/web/auth/app/auth/two-factor-expired/route.ts.ejs +22 -0
  103. package/templates/features/web/auth/components/auth/login-form.tsx.ejs +18 -0
  104. package/templates/features/web/auth/components/auth/two-factor-verify.tsx.ejs +173 -0
  105. package/templates/features/web/auth/components/settings/set-password-form.tsx.ejs +123 -0
  106. package/templates/features/web/auth/components/settings/two-factor-manage.tsx.ejs +253 -0
  107. package/templates/features/web/auth/components/settings/two-factor-setup.tsx.ejs +249 -0
  108. package/templates/features/web/auth/components/ui/checkbox.tsx +32 -0
  109. package/templates/features/web/auth/components/ui/dialog.tsx +143 -0
  110. package/templates/features/web/auth/components/ui/input-otp.tsx +71 -0
  111. package/templates/integrations/mobile/adjust/store/{adjust.store.ts → adjust-store.ts} +3 -3
  112. package/templates/integrations/mobile/att/store/{att.store.ts → att-store.ts} +2 -2
  113. package/templates/integrations/mobile/revenuecat/store/{revenuecat.store.ts → revenuecat-store.ts} +1 -1
  114. package/templates/integrations/mobile/scate/store/{scate.store.ts → scate-store.ts} +1 -1
  115. package/templates/base/mobile/src/components/ui/index.ts +0 -6
  116. package/templates/base/mobile/src/store/index.ts.ejs +0 -18
  117. package/templates/base/web/src/lib/auth/index.ts.ejs +0 -40
  118. package/templates/features/mobile/auth/components/auth/index.ts +0 -2
  119. package/templates/features/mobile/auth/hooks/index.ts.ejs +0 -1
  120. /package/templates/base/mobile/src/services/{errorService.ts → error-service.ts} +0 -0
  121. /package/templates/base/mobile/src/store/{ui.store.ts → ui-store.ts} +0 -0
  122. /package/templates/features/web/auth/app/{(app) → (protected)}/settings/sessions/sessions-client.tsx.ejs +0 -0
  123. /package/templates/integrations/mobile/adjust/services/{adjustService.ts.ejs → adjust-service.ts.ejs} +0 -0
  124. /package/templates/integrations/mobile/att/services/{attService.ts → att-service.ts} +0 -0
  125. /package/templates/integrations/mobile/att/services/{trackingPermissions.ts → tracking-permissions.ts} +0 -0
  126. /package/templates/integrations/mobile/revenuecat/services/{revenuecatService.ts.ejs → revenuecat-service.ts.ejs} +0 -0
  127. /package/templates/integrations/mobile/scate/services/{scateService.ts.ejs → scate-service.ts.ejs} +0 -0
@@ -21,13 +21,23 @@ const protectedRoutes = ["/dashboard", "/settings", "/profile"];
21
21
  // Routes that should redirect to dashboard if already authenticated
22
22
  const authRoutes = ["/login", "/register", "/forgot-password"];
23
23
 
24
- // Routes that are always public
25
- const publicRoutes = ["/", "/auth/callback", "/reset-password", "/verify-email"];
24
+ // Routes that are always public (including 2FA verification which needs session but shouldn't redirect)
25
+ const publicRoutes = ["/", "/auth/callback", "/auth/session-expired", "/reset-password", "/verify-email"<% if (features.authentication.twoFactor) { %>, "/login/two-factor", "/auth/two-factor-expired"<% } %>];
26
26
 
27
27
  export function proxy(request: NextRequest) {
28
28
  const { pathname } = request.nextUrl;
29
29
  const sessionToken = request.cookies.get(COOKIE_NAMES.SESSION)?.value;
30
30
 
31
+ // Check if current path matches any public routes (these always pass through)
32
+ const isPublicRoute = publicRoutes.some(
33
+ (route) => pathname === route || pathname.startsWith(`${route}/`)
34
+ );
35
+
36
+ // Public routes always pass through
37
+ if (isPublicRoute) {
38
+ return NextResponse.next();
39
+ }
40
+
31
41
  // Check if current path matches any protected routes
32
42
  const isProtectedRoute = protectedRoutes.some(
33
43
  (route) => pathname === route || pathname.startsWith(`${route}/`)
@@ -7,8 +7,8 @@ import {
7
7
  createDeviceSession,
8
8
  validateDeviceSession,
9
9
  updateDeviceActivity,
10
- type DeviceSession,
11
10
  } from "@/lib/device/actions";
11
+ import type { DeviceSession } from "@/lib/device/types";
12
12
 
13
13
  interface DeviceSessionState {
14
14
  // State
@@ -11,6 +11,13 @@ export default function AuthLayout() {
11
11
  >
12
12
  <Stack.Screen name="login" />
13
13
  <Stack.Screen name="register" />
14
+ <Stack.Screen name="two-factor" />
15
+ <Stack.Screen
16
+ name="two-factor-expired"
17
+ options={{
18
+ gestureEnabled: false,
19
+ }}
20
+ />
14
21
  </Stack>
15
22
  );
16
23
  }
@@ -1,10 +1,10 @@
1
1
  import React, { useEffect, useMemo } from 'react';
2
2
  import { View, StyleSheet, SafeAreaView, TouchableOpacity } from 'react-native';
3
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';
4
+ import { LoginForm } from '../../src/components/auth/login-form';
5
+ import { IconSymbol } from '../../src/components/ui/icon-symbol';
6
+ import { useAppTheme, AppTheme } from '@/context/theme-context';
7
+ import { useAuth } from '../../src/hooks/auth';
8
8
 
9
9
  export default function LoginScreen() {
10
10
  const theme = useAppTheme();
@@ -12,16 +12,17 @@ export default function LoginScreen() {
12
12
 
13
13
  const { isAuthenticated } = useAuth();
14
14
 
15
- // Watch for auth state changes (handles both native and browser OAuth)
15
+ // Watch for auth state changes - close modal when authenticated
16
+ // Root layout handles routing based on email verification status
16
17
  useEffect(() => {
17
18
  if (isAuthenticated) {
18
- router.replace('/(tabs)');
19
+ // Close the auth modal - root layout will handle navigation
20
+ router.back();
19
21
  }
20
22
  }, [isAuthenticated]);
21
23
 
22
24
  const handleLoginSuccess = () => {
23
- // Navigate to the main app (for email/password login)
24
- router.replace('/(tabs)');
25
+ // Navigation handled by useEffect watching isAuthenticated
25
26
  };
26
27
 
27
28
  const handleSwitchToRegister = () => {
@@ -1,10 +1,10 @@
1
1
  import React, { useEffect, useMemo } from 'react';
2
2
  import { View, StyleSheet, SafeAreaView, TouchableOpacity } from 'react-native';
3
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';
4
+ import { RegisterForm } from '../../src/components/auth/register-form';
5
+ import { IconSymbol } from '../../src/components/ui/icon-symbol';
6
+ import { useAppTheme, AppTheme } from '@/context/theme-context';
7
+ import { useAuth } from '../../src/hooks/auth';
8
8
 
9
9
  export default function RegisterScreen() {
10
10
  const theme = useAppTheme();
@@ -12,16 +12,17 @@ export default function RegisterScreen() {
12
12
 
13
13
  const { isAuthenticated } = useAuth();
14
14
 
15
- // Watch for auth state changes (handles both native and browser OAuth)
15
+ // Watch for auth state changes - close modal when authenticated
16
+ // Root layout handles routing based on email verification status
16
17
  useEffect(() => {
17
18
  if (isAuthenticated) {
18
- router.replace('/(tabs)');
19
+ // Close the auth modal - root layout will handle navigation
20
+ router.back();
19
21
  }
20
22
  }, [isAuthenticated]);
21
23
 
22
24
  const handleRegisterSuccess = () => {
23
- // Navigate to the main app (for email/password registration)
24
- router.replace('/(tabs)');
25
+ // Navigation handled by useEffect watching isAuthenticated
25
26
  };
26
27
 
27
28
  const handleSwitchToLogin = () => {
@@ -0,0 +1,88 @@
1
+ import React, { useMemo } from 'react';
2
+ import { View, Text, StyleSheet, SafeAreaView } from 'react-native';
3
+ import { useRouter } from 'expo-router';
4
+ import { Button } from '../../src/components/ui/button';
5
+ import { IconSymbol } from '../../src/components/ui/icon-symbol';
6
+ import { useAppTheme, AppTheme } from '@/context/theme-context';
7
+ import { useAuth } from '../../src/hooks/auth';
8
+
9
+ export default function TwoFactorExpiredScreen() {
10
+ const router = useRouter();
11
+ const theme = useAppTheme();
12
+ const styles = useMemo(() => createStyles(theme), [theme]);
13
+ const { clearTwoFactorState } = useAuth();
14
+
15
+ const handleReturnToLogin = async () => {
16
+ // Clear the expired 2FA state before navigating back
17
+ await clearTwoFactorState();
18
+ // Navigate back to the existing login screen instead of creating a new one
19
+ // This prevents duplicate login modals in the navigation stack
20
+ if (router.canGoBack()) {
21
+ router.back();
22
+ }
23
+ };
24
+
25
+ return (
26
+ <SafeAreaView style={styles.container}>
27
+ <View style={styles.content}>
28
+ <View style={styles.iconContainer}>
29
+ <IconSymbol
30
+ name="exclamationmark.triangle.fill"
31
+ size={48}
32
+ color={theme.colors.warning ?? theme.colors.error}
33
+ />
34
+ </View>
35
+ <Text style={styles.title}>Verification Expired</Text>
36
+ <Text style={styles.description}>
37
+ Your two-factor verification session has expired for security reasons.
38
+ Please sign in again to continue.
39
+ </Text>
40
+ <Button
41
+ onPress={handleReturnToLogin}
42
+ title="Return to Login"
43
+ style={styles.button}
44
+ />
45
+ </View>
46
+ </SafeAreaView>
47
+ );
48
+ }
49
+
50
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
51
+ container: {
52
+ flex: 1,
53
+ backgroundColor: theme.colors.background,
54
+ },
55
+ content: {
56
+ flex: 1,
57
+ justifyContent: 'center',
58
+ alignItems: 'center',
59
+ padding: theme.spacing[6],
60
+ },
61
+ iconContainer: {
62
+ width: 80,
63
+ height: 80,
64
+ borderRadius: 40,
65
+ backgroundColor: (theme.colors.warning ?? theme.colors.error) + '15',
66
+ justifyContent: 'center',
67
+ alignItems: 'center',
68
+ marginBottom: theme.spacing[6],
69
+ },
70
+ title: {
71
+ fontSize: theme.typography.fontSize['2xl'],
72
+ fontWeight: 'bold',
73
+ color: theme.colors.text,
74
+ marginBottom: theme.spacing[2],
75
+ textAlign: 'center',
76
+ },
77
+ description: {
78
+ fontSize: theme.typography.fontSize.base,
79
+ color: theme.colors.textSecondary,
80
+ textAlign: 'center',
81
+ marginBottom: theme.spacing[6],
82
+ lineHeight: 24,
83
+ paddingHorizontal: theme.spacing[4],
84
+ },
85
+ button: {
86
+ minWidth: 200,
87
+ },
88
+ });
@@ -0,0 +1,259 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ SafeAreaView,
7
+ TouchableOpacity,
8
+ KeyboardAvoidingView,
9
+ Platform,
10
+ ScrollView,
11
+ Alert,
12
+ Switch,
13
+ } from 'react-native';
14
+ import { router } from 'expo-router';
15
+ import { Button } from '../../src/components/ui/button';
16
+ import { Input } from '../../src/components/ui/input';
17
+ import { IconSymbol } from '../../src/components/ui/icon-symbol';
18
+ import { useAppTheme, AppTheme } from '@/context/theme-context';
19
+ import { useAuth } from '../../src/hooks/auth';
20
+ import { useTwoFactorTimeout } from '../../src/hooks/two-factor-timeout';
21
+
22
+ export default function TwoFactorScreen() {
23
+ const theme = useAppTheme();
24
+ const styles = useMemo(() => createStyles(theme), [theme]);
25
+ const { verifyTotpLogin, verifyBackupCode, isLoading } = useAuth();
26
+ const { isExpired } = useTwoFactorTimeout();
27
+
28
+ const [code, setCode] = useState('');
29
+ const [trustDevice, setTrustDevice] = useState(false);
30
+ const [useBackupCode, setUseBackupCode] = useState(false);
31
+ const [error, setError] = useState<string | null>(null);
32
+
33
+ const handleVerify = async () => {
34
+ if (!code.trim()) {
35
+ setError('Please enter a code');
36
+ return;
37
+ }
38
+
39
+ // Check if session has expired before submitting
40
+ if (isExpired()) {
41
+ router.replace('/(auth)/two-factor-expired');
42
+ return;
43
+ }
44
+
45
+ setError(null);
46
+
47
+ const result = useBackupCode
48
+ ? await verifyBackupCode(code.trim())
49
+ : await verifyTotpLogin(code.trim(), trustDevice);
50
+
51
+ if (result.success) {
52
+ // Authentication complete - close auth modal
53
+ // Root layout will handle navigation based on auth state
54
+ if (router.canGoBack()) {
55
+ router.back();
56
+ }
57
+ } else {
58
+ Alert.alert('Verification Failed', result.error || 'Invalid code. Please try again.');
59
+ }
60
+ };
61
+
62
+ const handleBack = () => {
63
+ if (router.canGoBack()) {
64
+ router.back();
65
+ }
66
+ };
67
+
68
+ const toggleMode = () => {
69
+ setUseBackupCode(!useBackupCode);
70
+ setCode('');
71
+ setError(null);
72
+ };
73
+
74
+ return (
75
+ <SafeAreaView style={styles.container}>
76
+ {/* Back Button */}
77
+ <TouchableOpacity
78
+ style={styles.backButton}
79
+ onPress={handleBack}
80
+ activeOpacity={0.7}
81
+ >
82
+ <IconSymbol
83
+ name="chevron.left"
84
+ size={24}
85
+ color={theme.colors.text}
86
+ />
87
+ </TouchableOpacity>
88
+
89
+ <KeyboardAvoidingView
90
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
91
+ style={styles.keyboardView}
92
+ >
93
+ <ScrollView
94
+ contentContainerStyle={styles.scrollContent}
95
+ keyboardShouldPersistTaps="handled"
96
+ >
97
+ <View style={styles.form}>
98
+ {/* Icon */}
99
+ <View style={styles.iconContainer}>
100
+ <IconSymbol
101
+ name={useBackupCode ? 'key.fill' : 'lock.shield.fill'}
102
+ size={48}
103
+ color={theme.colors.primary}
104
+ />
105
+ </View>
106
+
107
+ <Text style={styles.title}>
108
+ {useBackupCode ? 'Enter Backup Code' : 'Two-Factor Authentication'}
109
+ </Text>
110
+ <Text style={styles.subtitle}>
111
+ {useBackupCode
112
+ ? 'Enter one of your backup codes to sign in'
113
+ : 'Enter the 6-digit code from your authenticator app'}
114
+ </Text>
115
+
116
+ <Input
117
+ label={useBackupCode ? 'Backup Code' : 'Verification Code'}
118
+ value={code}
119
+ onChangeText={(text) => {
120
+ // For TOTP, only allow digits and limit to 6
121
+ const value = useBackupCode
122
+ ? text
123
+ : text.replace(/\D/g, '').slice(0, 6);
124
+ setCode(value);
125
+ if (error) setError(null);
126
+ }}
127
+ placeholder={useBackupCode ? 'Enter backup code' : '000000'}
128
+ keyboardType={useBackupCode ? 'default' : 'number-pad'}
129
+ autoCapitalize="none"
130
+ autoComplete="one-time-code"
131
+ maxLength={useBackupCode ? 20 : 6}
132
+ error={error || undefined}
133
+ containerStyle={styles.inputContainer}
134
+ />
135
+
136
+ {!useBackupCode && (
137
+ <View style={styles.trustDeviceContainer}>
138
+ <Text style={styles.trustDeviceText}>
139
+ Trust this device for 30 days
140
+ </Text>
141
+ <Switch
142
+ value={trustDevice}
143
+ onValueChange={setTrustDevice}
144
+ trackColor={{
145
+ false: theme.colors.border,
146
+ true: theme.colors.primary,
147
+ }}
148
+ thumbColor={theme.colors.background}
149
+ />
150
+ </View>
151
+ )}
152
+
153
+ <Button
154
+ title="Verify"
155
+ onPress={handleVerify}
156
+ loading={isLoading}
157
+ disabled={isLoading}
158
+ style={styles.submitButton}
159
+ />
160
+
161
+ <TouchableOpacity
162
+ onPress={toggleMode}
163
+ style={styles.toggleButton}
164
+ activeOpacity={0.7}
165
+ >
166
+ <Text style={styles.toggleButtonText}>
167
+ {useBackupCode
168
+ ? 'Use authenticator app instead'
169
+ : 'Use a backup code instead'}
170
+ </Text>
171
+ </TouchableOpacity>
172
+ </View>
173
+ </ScrollView>
174
+ </KeyboardAvoidingView>
175
+ </SafeAreaView>
176
+ );
177
+ }
178
+
179
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
180
+ container: {
181
+ flex: 1,
182
+ backgroundColor: theme.colors.background,
183
+ },
184
+ backButton: {
185
+ position: 'absolute',
186
+ top: 60,
187
+ left: 16,
188
+ width: 44,
189
+ height: 44,
190
+ backgroundColor: theme.colors.backgroundSecondary,
191
+ borderRadius: 22,
192
+ justifyContent: 'center',
193
+ alignItems: 'center',
194
+ zIndex: 10,
195
+ ...theme.shadows.small,
196
+ },
197
+ keyboardView: {
198
+ flex: 1,
199
+ },
200
+ scrollContent: {
201
+ flexGrow: 1,
202
+ justifyContent: 'center',
203
+ padding: theme.spacing[5],
204
+ },
205
+ form: {
206
+ width: '100%',
207
+ maxWidth: 400,
208
+ alignSelf: 'center',
209
+ },
210
+ iconContainer: {
211
+ width: 80,
212
+ height: 80,
213
+ borderRadius: 40,
214
+ backgroundColor: theme.colors.primary + '15',
215
+ justifyContent: 'center',
216
+ alignItems: 'center',
217
+ alignSelf: 'center',
218
+ marginBottom: theme.spacing[6],
219
+ },
220
+ title: {
221
+ fontSize: theme.typography.fontSize['2xl'],
222
+ fontWeight: 'bold',
223
+ color: theme.colors.text,
224
+ textAlign: 'center',
225
+ marginBottom: theme.spacing[2],
226
+ },
227
+ subtitle: {
228
+ fontSize: theme.typography.fontSize.base,
229
+ color: theme.colors.textSecondary,
230
+ textAlign: 'center',
231
+ marginBottom: theme.spacing[8],
232
+ },
233
+ inputContainer: {
234
+ marginBottom: theme.spacing[4],
235
+ },
236
+ trustDeviceContainer: {
237
+ flexDirection: 'row',
238
+ justifyContent: 'space-between',
239
+ alignItems: 'center',
240
+ marginBottom: theme.spacing[6],
241
+ paddingHorizontal: theme.spacing[1],
242
+ },
243
+ trustDeviceText: {
244
+ fontSize: theme.typography.fontSize.sm,
245
+ color: theme.colors.textSecondary,
246
+ },
247
+ submitButton: {
248
+ marginBottom: theme.spacing[4],
249
+ },
250
+ toggleButton: {
251
+ alignItems: 'center',
252
+ paddingVertical: theme.spacing[2],
253
+ },
254
+ toggleButtonText: {
255
+ fontSize: theme.typography.fontSize.sm,
256
+ color: theme.colors.primary,
257
+ fontWeight: '500',
258
+ },
259
+ });
@@ -0,0 +1,9 @@
1
+ import { Stack } from 'expo-router';
2
+
3
+ export default function PublicLayout() {
4
+ return (
5
+ <Stack screenOptions={{ headerShown: false }}>
6
+ <Stack.Screen name="index" />
7
+ </Stack>
8
+ );
9
+ }