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
@@ -0,0 +1,147 @@
1
+ import React, { useState, useCallback, useMemo } from 'react';
2
+ import { View, Text, StyleSheet, ScrollView } from 'react-native';
3
+ import { useRouter, useFocusEffect } from 'expo-router';
4
+ import { useAuth } from '../../../../src/hooks/auth';
5
+ import { Button } from '../../../../src/components/ui/button';
6
+ import { IconSymbol } from '../../../../src/components/ui/icon-symbol';
7
+ import { useAppTheme, AppTheme } from '@/context/theme-context';
8
+
9
+ type FlowStep = 'idle' | 'checking' | 'set-password' | 'setup-2fa';
10
+
11
+ export default function SecuritySettingsScreen() {
12
+ const router = useRouter();
13
+ const theme = useAppTheme();
14
+ const styles = useMemo(() => createStyles(theme), [theme]);
15
+ const { user, checkHasPassword } = useAuth();
16
+
17
+ const [flowStep, setFlowStep] = useState<FlowStep>('idle');
18
+
19
+ const twoFactorEnabled = user?.twoFactorEnabled ?? false;
20
+
21
+ // Refresh state when screen comes into focus
22
+ useFocusEffect(
23
+ useCallback(() => {
24
+ setFlowStep('idle');
25
+ }, [])
26
+ );
27
+
28
+ const handleEnableTwoFactor = async () => {
29
+ setFlowStep('checking');
30
+ const hasPassword = await checkHasPassword();
31
+
32
+ if (!hasPassword) {
33
+ // OAuth user without password - must set one first
34
+ setFlowStep('set-password');
35
+ router.push('/(tabs)/settings/security/set-password');
36
+ } else {
37
+ setFlowStep('setup-2fa');
38
+ router.push('/(tabs)/settings/security/two-factor-setup');
39
+ }
40
+ };
41
+
42
+ const handleManageTwoFactor = () => {
43
+ router.push('/(tabs)/settings/security/two-factor-manage');
44
+ };
45
+
46
+ return (
47
+ <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
48
+ <View style={styles.card}>
49
+ <View style={[
50
+ styles.iconContainer,
51
+ { backgroundColor: twoFactorEnabled
52
+ ? (theme.mode === 'dark' ? 'rgba(74, 222, 128, 0.15)' : 'rgba(34, 197, 94, 0.1)')
53
+ : (theme.mode === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)')
54
+ }
55
+ ]}>
56
+ {twoFactorEnabled ? (
57
+ <IconSymbol name="checkmark.shield.fill" size={28} color={theme.colors.success ?? theme.colors.primary} />
58
+ ) : (
59
+ <IconSymbol name="shield.slash.fill" size={28} color={theme.colors.textSecondary} />
60
+ )}
61
+ </View>
62
+
63
+ <Text style={styles.cardTitle}>Two-Factor Authentication</Text>
64
+
65
+ <Text style={styles.cardDescription}>
66
+ {twoFactorEnabled
67
+ ? 'Your account is protected with two-factor authentication.'
68
+ : 'Add an extra layer of security to your account by requiring a verification code in addition to your password.'}
69
+ </Text>
70
+
71
+ {twoFactorEnabled ? (
72
+ <Button
73
+ onPress={handleManageTwoFactor}
74
+ title="Manage 2FA"
75
+ fullWidth
76
+ />
77
+ ) : (
78
+ <Button
79
+ onPress={handleEnableTwoFactor}
80
+ title="Enable 2FA"
81
+ loading={flowStep === 'checking'}
82
+ fullWidth
83
+ />
84
+ )}
85
+ </View>
86
+
87
+ <View style={styles.noteContainer}>
88
+ <IconSymbol name="info.circle.fill" size={16} color={theme.colors.textMuted} />
89
+ <Text style={styles.note}>
90
+ Two-factor authentication only applies when signing in with email and password. OAuth providers handle their own security.
91
+ </Text>
92
+ </View>
93
+ </ScrollView>
94
+ );
95
+ }
96
+
97
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
98
+ container: {
99
+ flex: 1,
100
+ backgroundColor: theme.colors.background,
101
+ },
102
+ scrollContent: {
103
+ padding: theme.spacing[5],
104
+ },
105
+ card: {
106
+ padding: theme.spacing[6],
107
+ borderRadius: 20,
108
+ backgroundColor: theme.colors.backgroundSecondary,
109
+ marginBottom: theme.spacing[4],
110
+ alignItems: 'center',
111
+ ...theme.shadows.small,
112
+ },
113
+ iconContainer: {
114
+ width: 64,
115
+ height: 64,
116
+ borderRadius: 16,
117
+ justifyContent: 'center',
118
+ alignItems: 'center',
119
+ marginBottom: theme.spacing[4],
120
+ },
121
+ cardTitle: {
122
+ fontSize: 20,
123
+ fontWeight: '600',
124
+ color: theme.colors.text,
125
+ marginBottom: theme.spacing[2],
126
+ textAlign: 'center',
127
+ },
128
+ cardDescription: {
129
+ fontSize: theme.typography.fontSize.sm,
130
+ color: theme.colors.textSecondary,
131
+ lineHeight: 22,
132
+ marginBottom: theme.spacing[5],
133
+ textAlign: 'center',
134
+ },
135
+ noteContainer: {
136
+ flexDirection: 'row',
137
+ alignItems: 'flex-start',
138
+ paddingHorizontal: theme.spacing[2],
139
+ gap: theme.spacing[2],
140
+ },
141
+ note: {
142
+ flex: 1,
143
+ fontSize: theme.typography.fontSize.xs,
144
+ color: theme.colors.textMuted,
145
+ lineHeight: 18,
146
+ },
147
+ });
@@ -0,0 +1,140 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { View, Text, StyleSheet, ScrollView } from 'react-native';
3
+ import { useRouter } from 'expo-router';
4
+ import { useAuth } from '../../../../src/hooks/auth';
5
+ import { Button } from '../../../../src/components/ui/button';
6
+ import { Input } from '../../../../src/components/ui/input';
7
+ import { IconSymbol } from '../../../../src/components/ui/icon-symbol';
8
+ import { useAppTheme, AppTheme } from '@/context/theme-context';
9
+
10
+ export default function SetPasswordScreen() {
11
+ const router = useRouter();
12
+ const theme = useAppTheme();
13
+ const styles = useMemo(() => createStyles(theme), [theme]);
14
+ const { setInitialPassword, isLoading } = useAuth();
15
+
16
+ const [password, setPassword] = useState('');
17
+ const [confirmPassword, setConfirmPassword] = useState('');
18
+ const [error, setError] = useState<string | null>(null);
19
+
20
+ const handleSubmit = async () => {
21
+ setError(null);
22
+
23
+ if (password.length < 8) {
24
+ setError('Password must be at least 8 characters');
25
+ return;
26
+ }
27
+
28
+ if (password !== confirmPassword) {
29
+ setError('Passwords do not match');
30
+ return;
31
+ }
32
+
33
+ const result = await setInitialPassword(password);
34
+
35
+ if (!result.success) {
36
+ setError(result.error || 'Failed to set password');
37
+ return;
38
+ }
39
+
40
+ // Navigate to 2FA setup after password is set
41
+ router.replace('/(tabs)/settings/security/two-factor-setup');
42
+ };
43
+
44
+ return (
45
+ <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
46
+ <View style={styles.iconContainer}>
47
+ <IconSymbol name="lock.fill" size={48} color={theme.colors.primary} />
48
+ </View>
49
+ <Text style={styles.title}>Set a Password</Text>
50
+ <Text style={styles.description}>
51
+ To enable two-factor authentication, you need to set a password for your account.
52
+ </Text>
53
+
54
+ <View style={styles.infoBox}>
55
+ <IconSymbol name="info.circle.fill" size={16} color={theme.colors.primary} />
56
+ <Text style={styles.infoText}>
57
+ Two-factor authentication only applies when signing in with email and password.
58
+ OAuth providers (Google, Apple, etc.) handle their own security verification.
59
+ </Text>
60
+ </View>
61
+
62
+ <Input
63
+ label="New Password"
64
+ value={password}
65
+ onChangeText={setPassword}
66
+ secureTextEntry
67
+ autoCapitalize="none"
68
+ containerStyle={styles.inputContainer}
69
+ />
70
+
71
+ <Input
72
+ label="Confirm Password"
73
+ value={confirmPassword}
74
+ onChangeText={setConfirmPassword}
75
+ secureTextEntry
76
+ autoCapitalize="none"
77
+ error={error || undefined}
78
+ containerStyle={styles.inputContainer}
79
+ />
80
+
81
+ <Button
82
+ onPress={handleSubmit}
83
+ title="Set Password & Continue"
84
+ loading={isLoading}
85
+ disabled={!password || !confirmPassword}
86
+ />
87
+ </ScrollView>
88
+ );
89
+ }
90
+
91
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
92
+ container: {
93
+ flex: 1,
94
+ backgroundColor: theme.colors.background,
95
+ },
96
+ scrollContent: {
97
+ padding: theme.spacing[6],
98
+ },
99
+ iconContainer: {
100
+ width: 80,
101
+ height: 80,
102
+ borderRadius: 40,
103
+ backgroundColor: theme.colors.primary + '15',
104
+ justifyContent: 'center',
105
+ alignItems: 'center',
106
+ alignSelf: 'center',
107
+ marginBottom: theme.spacing[6],
108
+ },
109
+ title: {
110
+ fontSize: theme.typography.fontSize['2xl'],
111
+ fontWeight: 'bold',
112
+ color: theme.colors.text,
113
+ textAlign: 'center',
114
+ marginBottom: theme.spacing[2],
115
+ },
116
+ description: {
117
+ fontSize: theme.typography.fontSize.base,
118
+ color: theme.colors.textSecondary,
119
+ textAlign: 'center',
120
+ marginBottom: theme.spacing[6],
121
+ lineHeight: 24,
122
+ },
123
+ infoBox: {
124
+ flexDirection: 'row',
125
+ padding: theme.spacing[4],
126
+ borderRadius: 12,
127
+ backgroundColor: theme.colors.backgroundSecondary,
128
+ marginBottom: theme.spacing[6],
129
+ },
130
+ infoText: {
131
+ flex: 1,
132
+ fontSize: theme.typography.fontSize.xs,
133
+ color: theme.colors.textSecondary,
134
+ lineHeight: 18,
135
+ marginLeft: theme.spacing[3],
136
+ },
137
+ inputContainer: {
138
+ marginBottom: theme.spacing[4],
139
+ },
140
+ });
@@ -0,0 +1,366 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { View, Text, StyleSheet, ScrollView, Alert, Modal } from 'react-native';
3
+ import { useRouter } from 'expo-router';
4
+ import { File, Paths } from 'expo-file-system/next';
5
+ import * as Sharing from 'expo-sharing';
6
+ import { useAuth } from '../../../../src/hooks/auth';
7
+ import { Button } from '../../../../src/components/ui/button';
8
+ import { Input } from '../../../../src/components/ui/input';
9
+ import { IconSymbol } from '../../../../src/components/ui/icon-symbol';
10
+ import { useAppTheme, AppTheme } from '@/context/theme-context';
11
+
12
+ export default function TwoFactorManageScreen() {
13
+ const router = useRouter();
14
+ const theme = useAppTheme();
15
+ const styles = useMemo(() => createStyles(theme), [theme]);
16
+ const { generateBackupCodes, disableTwoFactor, isLoading } = useAuth();
17
+
18
+ const [backupModalVisible, setBackupModalVisible] = useState(false);
19
+ const [disableModalVisible, setDisableModalVisible] = useState(false);
20
+ const [password, setPassword] = useState('');
21
+ const [newBackupCodes, setNewBackupCodes] = useState<string[]>([]);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ const handleGenerateBackupCodes = async () => {
25
+ setError(null);
26
+ const result = await generateBackupCodes(password);
27
+
28
+ if (!result.success) {
29
+ setError(result.error || 'Failed to generate codes');
30
+ return;
31
+ }
32
+
33
+ setNewBackupCodes(result.backupCodes || []);
34
+ setPassword('');
35
+ };
36
+
37
+ const handleDownloadCodes = async () => {
38
+ const content = [
39
+ 'Two-Factor Authentication Backup Codes',
40
+ '========================================',
41
+ '',
42
+ 'Keep these codes safe. Each code can only be used once.',
43
+ '',
44
+ ...newBackupCodes,
45
+ ].join('\n');
46
+
47
+ const file = new File(Paths.cache, '2fa-backup-codes.txt');
48
+ file.write(content);
49
+ await Sharing.shareAsync(file.uri);
50
+ };
51
+
52
+ const handleDisableTwoFactor = async () => {
53
+ setError(null);
54
+ const result = await disableTwoFactor(password);
55
+
56
+ if (!result.success) {
57
+ setError(result.error || 'Failed to disable 2FA');
58
+ return;
59
+ }
60
+
61
+ Alert.alert('Success', 'Two-factor authentication has been disabled.');
62
+ setDisableModalVisible(false);
63
+ setPassword('');
64
+ router.back();
65
+ };
66
+
67
+ const closeBackupModal = () => {
68
+ setBackupModalVisible(false);
69
+ setNewBackupCodes([]);
70
+ setPassword('');
71
+ setError(null);
72
+ };
73
+
74
+ const closeDisableModal = () => {
75
+ setDisableModalVisible(false);
76
+ setPassword('');
77
+ setError(null);
78
+ };
79
+
80
+ return (
81
+ <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
82
+ {/* Status Banner */}
83
+ <View style={[
84
+ styles.statusBanner,
85
+ { backgroundColor: theme.mode === 'dark' ? 'rgba(74, 222, 128, 0.1)' : 'rgba(34, 197, 94, 0.08)' }
86
+ ]}>
87
+ <View style={[
88
+ styles.statusIconContainer,
89
+ { backgroundColor: theme.mode === 'dark' ? 'rgba(74, 222, 128, 0.2)' : 'rgba(34, 197, 94, 0.15)' }
90
+ ]}>
91
+ <IconSymbol name="checkmark.shield.fill" size={24} color={theme.colors.success ?? theme.colors.primary} />
92
+ </View>
93
+ <Text style={[styles.statusText, { color: theme.colors.success }]}>
94
+ 2FA is Active
95
+ </Text>
96
+ </View>
97
+
98
+ {/* Generate Backup Codes */}
99
+ <View style={styles.card}>
100
+ <View style={styles.cardHeader}>
101
+ <View style={[styles.cardIconContainer, { backgroundColor: theme.mode === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)' }]}>
102
+ <IconSymbol name="key.fill" size={20} color={theme.colors.text} />
103
+ </View>
104
+ <View style={styles.cardHeaderText}>
105
+ <Text style={styles.cardTitle}>Backup Codes</Text>
106
+ <Text style={styles.cardSubtitle}>Recovery options for your account</Text>
107
+ </View>
108
+ </View>
109
+ <Text style={styles.cardDescription}>
110
+ Generate new backup codes. This will invalidate any previously generated codes.
111
+ </Text>
112
+ <Button
113
+ onPress={() => setBackupModalVisible(true)}
114
+ title="Generate New Codes"
115
+ fullWidth
116
+ />
117
+ </View>
118
+
119
+ {/* Disable 2FA */}
120
+ <View style={styles.card}>
121
+ <View style={styles.cardHeader}>
122
+ <View style={[styles.cardIconContainer, { backgroundColor: theme.mode === 'dark' ? 'rgba(248, 113, 113, 0.15)' : 'rgba(239, 68, 68, 0.08)' }]}>
123
+ <IconSymbol name="shield.slash.fill" size={20} color={theme.colors.error} />
124
+ </View>
125
+ <View style={styles.cardHeaderText}>
126
+ <Text style={styles.cardTitle}>Disable 2FA</Text>
127
+ <Text style={styles.cardSubtitle}>Remove extra security</Text>
128
+ </View>
129
+ </View>
130
+ <Text style={styles.cardDescription}>
131
+ Remove two-factor authentication from your account. This will make your account less secure.
132
+ </Text>
133
+ <Button
134
+ onPress={() => setDisableModalVisible(true)}
135
+ title="Disable 2FA"
136
+ variant="destructive"
137
+ fullWidth
138
+ />
139
+ </View>
140
+
141
+ {/* Backup Codes Modal */}
142
+ <Modal visible={backupModalVisible} animationType="slide" presentationStyle="pageSheet">
143
+ <View style={styles.modalContainer}>
144
+ <View style={[styles.modalIconContainer, { backgroundColor: theme.mode === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)' }]}>
145
+ <IconSymbol name="key.fill" size={32} color={theme.colors.text} />
146
+ </View>
147
+ <Text style={styles.modalTitle}>Generate Backup Codes</Text>
148
+
149
+ {newBackupCodes.length === 0 ? (
150
+ <>
151
+ <Text style={styles.modalDescription}>
152
+ Enter your password to generate new backup codes.
153
+ </Text>
154
+ <Input
155
+ label="Password"
156
+ value={password}
157
+ onChangeText={setPassword}
158
+ secureTextEntry
159
+ error={error || undefined}
160
+ containerStyle={styles.inputContainer}
161
+ />
162
+ <Button
163
+ onPress={handleGenerateBackupCodes}
164
+ title="Generate Codes"
165
+ loading={isLoading}
166
+ disabled={!password}
167
+ fullWidth
168
+ />
169
+ </>
170
+ ) : (
171
+ <>
172
+ <Text style={styles.modalDescription}>
173
+ Save these codes in a secure location.
174
+ </Text>
175
+ <View style={styles.codesContainer}>
176
+ {newBackupCodes.map((code, index) => (
177
+ <Text key={index} style={styles.code}>{code}</Text>
178
+ ))}
179
+ </View>
180
+ <Button
181
+ onPress={handleDownloadCodes}
182
+ title="Download Codes"
183
+ style={styles.downloadButton}
184
+ fullWidth
185
+ />
186
+ </>
187
+ )}
188
+
189
+ <Button
190
+ onPress={closeBackupModal}
191
+ title={newBackupCodes.length > 0 ? 'Done' : 'Cancel'}
192
+ variant="ghost"
193
+ fullWidth
194
+ />
195
+ </View>
196
+ </Modal>
197
+
198
+ {/* Disable Modal */}
199
+ <Modal visible={disableModalVisible} animationType="slide" presentationStyle="pageSheet">
200
+ <View style={styles.modalContainer}>
201
+ <View style={[styles.modalIconContainer, { backgroundColor: theme.mode === 'dark' ? 'rgba(248, 113, 113, 0.15)' : 'rgba(239, 68, 68, 0.1)' }]}>
202
+ <IconSymbol name="shield.slash.fill" size={32} color={theme.colors.error} />
203
+ </View>
204
+ <Text style={styles.modalTitle}>Disable 2FA</Text>
205
+ <Text style={styles.warningText}>
206
+ Warning: This will make your account less secure.
207
+ </Text>
208
+ <Text style={styles.modalDescription}>
209
+ Enter your password to confirm.
210
+ </Text>
211
+
212
+ <Input
213
+ label="Password"
214
+ value={password}
215
+ onChangeText={setPassword}
216
+ secureTextEntry
217
+ error={error || undefined}
218
+ containerStyle={styles.inputContainer}
219
+ />
220
+
221
+ <Button
222
+ onPress={handleDisableTwoFactor}
223
+ title="Disable 2FA"
224
+ variant="destructive"
225
+ loading={isLoading}
226
+ disabled={!password}
227
+ fullWidth
228
+ />
229
+ <Button
230
+ onPress={closeDisableModal}
231
+ title="Cancel"
232
+ variant="ghost"
233
+ fullWidth
234
+ />
235
+ </View>
236
+ </Modal>
237
+ </ScrollView>
238
+ );
239
+ }
240
+
241
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
242
+ container: {
243
+ flex: 1,
244
+ backgroundColor: theme.colors.background,
245
+ },
246
+ scrollContent: {
247
+ padding: theme.spacing[5],
248
+ },
249
+ statusBanner: {
250
+ flexDirection: 'row',
251
+ alignItems: 'center',
252
+ padding: theme.spacing[4],
253
+ borderRadius: 16,
254
+ marginBottom: theme.spacing[5],
255
+ },
256
+ statusIconContainer: {
257
+ width: 40,
258
+ height: 40,
259
+ borderRadius: 10,
260
+ justifyContent: 'center',
261
+ alignItems: 'center',
262
+ },
263
+ statusText: {
264
+ fontSize: theme.typography.fontSize.base,
265
+ fontWeight: '600',
266
+ marginLeft: theme.spacing[3],
267
+ },
268
+ card: {
269
+ padding: theme.spacing[5],
270
+ borderRadius: 20,
271
+ backgroundColor: theme.colors.backgroundSecondary,
272
+ marginBottom: theme.spacing[4],
273
+ ...theme.shadows.small,
274
+ },
275
+ cardHeader: {
276
+ flexDirection: 'row',
277
+ alignItems: 'center',
278
+ marginBottom: theme.spacing[3],
279
+ },
280
+ cardIconContainer: {
281
+ width: 44,
282
+ height: 44,
283
+ borderRadius: 12,
284
+ justifyContent: 'center',
285
+ alignItems: 'center',
286
+ },
287
+ cardHeaderText: {
288
+ marginLeft: theme.spacing[3],
289
+ flex: 1,
290
+ },
291
+ cardTitle: {
292
+ fontSize: theme.typography.fontSize.base,
293
+ fontWeight: '600',
294
+ color: theme.colors.text,
295
+ },
296
+ cardSubtitle: {
297
+ fontSize: theme.typography.fontSize.xs,
298
+ color: theme.colors.textSecondary,
299
+ marginTop: 2,
300
+ },
301
+ cardDescription: {
302
+ fontSize: theme.typography.fontSize.sm,
303
+ color: theme.colors.textSecondary,
304
+ lineHeight: 20,
305
+ marginBottom: theme.spacing[4],
306
+ },
307
+ modalContainer: {
308
+ flex: 1,
309
+ backgroundColor: theme.colors.background,
310
+ padding: theme.spacing[6],
311
+ justifyContent: 'center',
312
+ },
313
+ modalIconContainer: {
314
+ width: 72,
315
+ height: 72,
316
+ borderRadius: 18,
317
+ justifyContent: 'center',
318
+ alignItems: 'center',
319
+ alignSelf: 'center',
320
+ marginBottom: theme.spacing[5],
321
+ },
322
+ modalTitle: {
323
+ fontSize: 24,
324
+ fontWeight: '700',
325
+ color: theme.colors.text,
326
+ textAlign: 'center',
327
+ marginBottom: theme.spacing[2],
328
+ letterSpacing: -0.3,
329
+ },
330
+ modalDescription: {
331
+ fontSize: theme.typography.fontSize.base,
332
+ color: theme.colors.textSecondary,
333
+ textAlign: 'center',
334
+ marginBottom: theme.spacing[6],
335
+ lineHeight: 22,
336
+ },
337
+ warningText: {
338
+ fontSize: theme.typography.fontSize.sm,
339
+ fontWeight: '500',
340
+ color: theme.colors.error,
341
+ textAlign: 'center',
342
+ marginBottom: theme.spacing[2],
343
+ },
344
+ inputContainer: {
345
+ marginBottom: theme.spacing[4],
346
+ },
347
+ codesContainer: {
348
+ padding: theme.spacing[4],
349
+ borderRadius: 16,
350
+ backgroundColor: theme.colors.backgroundSecondary,
351
+ marginBottom: theme.spacing[6],
352
+ flexDirection: 'row',
353
+ flexWrap: 'wrap',
354
+ justifyContent: 'space-between',
355
+ },
356
+ code: {
357
+ fontFamily: 'monospace',
358
+ fontSize: theme.typography.fontSize.sm,
359
+ color: theme.colors.text,
360
+ width: '48%',
361
+ marginBottom: theme.spacing[2],
362
+ },
363
+ downloadButton: {
364
+ marginBottom: theme.spacing[4],
365
+ },
366
+ });