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,317 @@
1
+ import React, { useState, useCallback, useMemo } from 'react';
2
+ import { View, Text, StyleSheet, Alert, ScrollView, TouchableOpacity } from 'react-native';
3
+ import { useRouter } from 'expo-router';
4
+ import QRCode from 'react-native-qrcode-svg';
5
+ import * as Clipboard from 'expo-clipboard';
6
+ import * as FileSystem from 'expo-file-system';
7
+ import * as Sharing from 'expo-sharing';
8
+ import { useAuth } from '../../../../src/hooks/auth';
9
+ import { Button } from '../../../../src/components/ui/button';
10
+ import { Input } from '../../../../src/components/ui/input';
11
+ import { IconSymbol } from '../../../../src/components/ui/icon-symbol';
12
+ import { useAppTheme, AppTheme } from '@/context/theme-context';
13
+
14
+ type SetupStep = 'password' | 'qr-code' | 'verify' | 'backup-codes';
15
+
16
+ export default function TwoFactorSetupScreen() {
17
+ const router = useRouter();
18
+ const theme = useAppTheme();
19
+ const styles = useMemo(() => createStyles(theme), [theme]);
20
+ const { enableTwoFactor, verifyTotpSetup, isLoading } = useAuth();
21
+
22
+ const [step, setStep] = useState<SetupStep>('password');
23
+ const [password, setPassword] = useState('');
24
+ const [totpURI, setTotpURI] = useState<string | null>(null);
25
+ const [backupCodes, setBackupCodes] = useState<string[]>([]);
26
+ const [verificationCode, setVerificationCode] = useState('');
27
+ const [error, setError] = useState<string | null>(null);
28
+
29
+ // Extract secret from TOTP URI for manual entry
30
+ const getSecretFromURI = useCallback((uri: string): string => {
31
+ const match = uri.match(/secret=([^&]+)/);
32
+ return match ? match[1] : '';
33
+ }, []);
34
+
35
+ const handlePasswordSubmit = async () => {
36
+ setError(null);
37
+ const result = await enableTwoFactor(password);
38
+
39
+ if (!result.success) {
40
+ setError(result.error || 'Failed to enable 2FA');
41
+ return;
42
+ }
43
+
44
+ setTotpURI(result.totpURI || null);
45
+ setBackupCodes(result.backupCodes || []);
46
+ setStep('qr-code');
47
+ };
48
+
49
+ const handleVerifyCode = async () => {
50
+ setError(null);
51
+
52
+ if (verificationCode.length !== 6) {
53
+ setError('Please enter a 6-digit code');
54
+ return;
55
+ }
56
+
57
+ const result = await verifyTotpSetup(verificationCode);
58
+
59
+ if (!result.success) {
60
+ setError(result.error || 'Verification failed');
61
+ return;
62
+ }
63
+
64
+ setStep('backup-codes');
65
+ };
66
+
67
+ const handleCopySecret = async () => {
68
+ if (!totpURI) return;
69
+ const secret = getSecretFromURI(totpURI);
70
+ await Clipboard.setStringAsync(secret);
71
+ Alert.alert('Copied', 'Secret key copied to clipboard');
72
+ };
73
+
74
+ const handleDownloadBackupCodes = async () => {
75
+ const content = [
76
+ 'Two-Factor Authentication Backup Codes',
77
+ '========================================',
78
+ '',
79
+ 'Keep these codes safe. Each code can only be used once.',
80
+ '',
81
+ ...backupCodes,
82
+ ].join('\n');
83
+
84
+ const fileUri = FileSystem.documentDirectory + '2fa-backup-codes.txt';
85
+ await FileSystem.writeAsStringAsync(fileUri, content);
86
+ await Sharing.shareAsync(fileUri);
87
+ };
88
+
89
+ const handleComplete = () => {
90
+ router.back();
91
+ };
92
+
93
+ // Step 1: Password confirmation
94
+ if (step === 'password') {
95
+ return (
96
+ <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
97
+ <View style={styles.iconContainer}>
98
+ <IconSymbol name="lock.shield.fill" size={48} color={theme.colors.primary} />
99
+ </View>
100
+ <Text style={styles.title}>Enable Two-Factor Authentication</Text>
101
+ <Text style={styles.description}>
102
+ Enter your password to begin setting up two-factor authentication.
103
+ </Text>
104
+
105
+ <Input
106
+ label="Password"
107
+ value={password}
108
+ onChangeText={setPassword}
109
+ secureTextEntry
110
+ autoCapitalize="none"
111
+ error={error || undefined}
112
+ containerStyle={styles.inputContainer}
113
+ />
114
+
115
+ <Button
116
+ onPress={handlePasswordSubmit}
117
+ title="Continue"
118
+ loading={isLoading}
119
+ disabled={!password}
120
+ />
121
+ </ScrollView>
122
+ );
123
+ }
124
+
125
+ // Step 2: QR Code scanning
126
+ if (step === 'qr-code') {
127
+ const secret = totpURI ? getSecretFromURI(totpURI) : '';
128
+
129
+ return (
130
+ <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
131
+ <Text style={styles.title}>Scan QR Code</Text>
132
+ <Text style={styles.description}>
133
+ Open your authenticator app and scan this QR code to add your account.
134
+ </Text>
135
+
136
+ {totpURI && (
137
+ <View style={styles.qrContainer}>
138
+ <QRCode value={totpURI} size={200} backgroundColor="#FFFFFF" />
139
+ </View>
140
+ )}
141
+
142
+ <Text style={styles.subtitle}>Or enter manually:</Text>
143
+ <View style={styles.secretContainer}>
144
+ <Text style={styles.secret} selectable>
145
+ {secret}
146
+ </Text>
147
+ <TouchableOpacity onPress={handleCopySecret} style={styles.copyButton}>
148
+ <IconSymbol name="doc.on.doc" size={20} color={theme.colors.primary} />
149
+ </TouchableOpacity>
150
+ </View>
151
+
152
+ <Button
153
+ onPress={() => setStep('verify')}
154
+ title="I've Added the Account"
155
+ style={styles.continueButton}
156
+ />
157
+ </ScrollView>
158
+ );
159
+ }
160
+
161
+ // Step 3: Verification
162
+ if (step === 'verify') {
163
+ return (
164
+ <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
165
+ <Text style={styles.title}>Verify Setup</Text>
166
+ <Text style={styles.description}>
167
+ Enter the 6-digit code from your authenticator app to verify the setup.
168
+ </Text>
169
+
170
+ <Input
171
+ label="Verification Code"
172
+ value={verificationCode}
173
+ onChangeText={(text) => setVerificationCode(text.replace(/\D/g, ''))}
174
+ placeholder="000000"
175
+ keyboardType="number-pad"
176
+ maxLength={6}
177
+ error={error || undefined}
178
+ containerStyle={styles.codeInputContainer}
179
+ />
180
+
181
+ <Button
182
+ onPress={handleVerifyCode}
183
+ title="Verify"
184
+ loading={isLoading}
185
+ disabled={verificationCode.length !== 6}
186
+ />
187
+ </ScrollView>
188
+ );
189
+ }
190
+
191
+ // Step 4: Backup codes
192
+ return (
193
+ <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
194
+ <View style={styles.iconContainer}>
195
+ <IconSymbol name="checkmark.circle.fill" size={48} color={theme.colors.success ?? theme.colors.primary} />
196
+ </View>
197
+ <Text style={styles.title}>Save Backup Codes</Text>
198
+ <Text style={styles.description}>
199
+ Save these backup codes in a secure location. Each code can only be used once
200
+ if you lose access to your authenticator app.
201
+ </Text>
202
+
203
+ <View style={styles.codesContainer}>
204
+ {backupCodes.map((code, index) => (
205
+ <Text key={index} style={styles.code}>
206
+ {code}
207
+ </Text>
208
+ ))}
209
+ </View>
210
+
211
+ <Button
212
+ onPress={handleDownloadBackupCodes}
213
+ title="Download Codes"
214
+ style={styles.downloadButton}
215
+ />
216
+
217
+ <Button
218
+ onPress={handleComplete}
219
+ title="Done"
220
+ variant="outline"
221
+ />
222
+ </ScrollView>
223
+ );
224
+ }
225
+
226
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
227
+ container: {
228
+ flex: 1,
229
+ backgroundColor: theme.colors.background,
230
+ },
231
+ scrollContent: {
232
+ padding: theme.spacing[6],
233
+ },
234
+ iconContainer: {
235
+ width: 80,
236
+ height: 80,
237
+ borderRadius: 40,
238
+ backgroundColor: theme.colors.primary + '15',
239
+ justifyContent: 'center',
240
+ alignItems: 'center',
241
+ alignSelf: 'center',
242
+ marginBottom: theme.spacing[6],
243
+ },
244
+ title: {
245
+ fontSize: theme.typography.fontSize['2xl'],
246
+ fontWeight: 'bold',
247
+ color: theme.colors.text,
248
+ textAlign: 'center',
249
+ marginBottom: theme.spacing[2],
250
+ },
251
+ subtitle: {
252
+ fontSize: theme.typography.fontSize.base,
253
+ fontWeight: '500',
254
+ color: theme.colors.text,
255
+ marginTop: theme.spacing[6],
256
+ marginBottom: theme.spacing[2],
257
+ },
258
+ description: {
259
+ fontSize: theme.typography.fontSize.base,
260
+ color: theme.colors.textSecondary,
261
+ textAlign: 'center',
262
+ marginBottom: theme.spacing[6],
263
+ lineHeight: 24,
264
+ },
265
+ inputContainer: {
266
+ marginBottom: theme.spacing[4],
267
+ },
268
+ codeInputContainer: {
269
+ marginBottom: theme.spacing[4],
270
+ },
271
+ qrContainer: {
272
+ alignItems: 'center',
273
+ padding: theme.spacing[6],
274
+ backgroundColor: '#FFFFFF', // White for QR scanability
275
+ borderRadius: 16,
276
+ alignSelf: 'center',
277
+ marginBottom: theme.spacing[6],
278
+ },
279
+ secretContainer: {
280
+ flexDirection: 'row',
281
+ alignItems: 'center',
282
+ justifyContent: 'center',
283
+ marginBottom: theme.spacing[6],
284
+ flexWrap: 'wrap',
285
+ },
286
+ secret: {
287
+ fontFamily: 'monospace',
288
+ fontSize: theme.typography.fontSize.sm,
289
+ color: theme.colors.text,
290
+ },
291
+ copyButton: {
292
+ marginLeft: theme.spacing[2],
293
+ padding: theme.spacing[2],
294
+ },
295
+ continueButton: {
296
+ marginTop: theme.spacing[4],
297
+ },
298
+ codesContainer: {
299
+ padding: theme.spacing[4],
300
+ borderRadius: 12,
301
+ backgroundColor: theme.colors.backgroundSecondary,
302
+ marginBottom: theme.spacing[6],
303
+ flexDirection: 'row',
304
+ flexWrap: 'wrap',
305
+ justifyContent: 'space-between',
306
+ },
307
+ code: {
308
+ fontFamily: 'monospace',
309
+ fontSize: theme.typography.fontSize.sm,
310
+ color: theme.colors.text,
311
+ width: '48%',
312
+ marginBottom: theme.spacing[2],
313
+ },
314
+ downloadButton: {
315
+ marginBottom: theme.spacing[4],
316
+ },
317
+ });
@@ -0,0 +1,331 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ ScrollView,
7
+ Modal,
8
+ Pressable,
9
+ Alert,
10
+ KeyboardAvoidingView,
11
+ Platform,
12
+ TouchableOpacity,
13
+ SafeAreaView,
14
+ } from 'react-native';
15
+ import { useRouter } from 'expo-router';
16
+ import { LinearGradient } from 'expo-linear-gradient';
17
+ import { useAuth } from '@/hooks/auth';
18
+ import { Button } from '@/components/ui/button';
19
+ import { Input } from '@/components/ui/input';
20
+ import { IconSymbol } from '@/components/ui/icon-symbol';
21
+ import { formatDisplayName } from '@/utils/formatters';
22
+ import { useAppTheme, AppTheme } from '@/context/theme-context';
23
+
24
+ export default function AccountScreen() {
25
+ const router = useRouter();
26
+ const theme = useAppTheme();
27
+ const styles = useMemo(() => createStyles(theme), [theme]);
28
+
29
+ const { user, signOut, deleteAccount } = useAuth();
30
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
31
+ const [deleteConfirmText, setDeleteConfirmText] = useState('');
32
+ const [isDeleting, setIsDeleting] = useState(false);
33
+
34
+ const gradientColors = theme.mode === 'dark'
35
+ ? [theme.colors.card, 'transparent'] as const
36
+ : ['#ffffff', '#f8fafc'] as const;
37
+
38
+ const handleDeleteAccount = async () => {
39
+ if (deleteConfirmText !== 'DELETE') {
40
+ return;
41
+ }
42
+
43
+ setIsDeleting(true);
44
+ const result = await deleteAccount();
45
+ setIsDeleting(false);
46
+
47
+ if (result.success) {
48
+ await signOut();
49
+ setShowDeleteModal(false);
50
+ setDeleteConfirmText('');
51
+ router.replace('/');
52
+ } else {
53
+ Alert.alert('Error', result.error || 'Failed to delete account. Please try again.');
54
+ }
55
+ };
56
+
57
+ const isDeleteButtonEnabled = deleteConfirmText === 'DELETE' && !isDeleting;
58
+
59
+ return (
60
+ <SafeAreaView style={styles.safeArea}>
61
+ {/* Custom Header */}
62
+ <View style={styles.header}>
63
+ <TouchableOpacity
64
+ onPress={() => router.dismiss()}
65
+ style={styles.backButton}
66
+ >
67
+ <IconSymbol name="chevron.left" size={24} color={theme.colors.text} />
68
+ </TouchableOpacity>
69
+ <Text style={styles.headerTitle}>Account</Text>
70
+ <View style={styles.headerSpacer} />
71
+ </View>
72
+
73
+ <ScrollView style={styles.container}>
74
+ <View style={styles.content}>
75
+
76
+ {/* User Info Section */}
77
+ {user && (
78
+ <LinearGradient
79
+ colors={gradientColors}
80
+ style={styles.userInfo}
81
+ start={{ x: 0, y: 0 }}
82
+ end={{ x: 0, y: 1 }}
83
+ >
84
+ <Text style={styles.greeting}>
85
+ {formatDisplayName(user.name)}
86
+ </Text>
87
+ <Text style={styles.email}>{user.email}</Text>
88
+ <Text style={styles.userId}>User ID: {user.id}</Text>
89
+ </LinearGradient>
90
+ )}
91
+
92
+ <View style={styles.dangerZone}>
93
+ <Text style={styles.dangerTitle}>Danger Zone</Text>
94
+ <Text style={styles.dangerDescription}>
95
+ Deleting your account is permanent and cannot be undone.
96
+ </Text>
97
+ <Button
98
+ title="Delete Account"
99
+ variant="destructive"
100
+ onPress={() => setShowDeleteModal(true)}
101
+ />
102
+ </View>
103
+
104
+ </View>
105
+
106
+ {/* Delete Account Confirmation Modal */}
107
+ <Modal
108
+ visible={showDeleteModal}
109
+ transparent
110
+ animationType="fade"
111
+ onRequestClose={() => setShowDeleteModal(false)}
112
+ statusBarTranslucent
113
+ >
114
+ <KeyboardAvoidingView
115
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
116
+ style={styles.keyboardAvoidingModal}
117
+ >
118
+ <Pressable
119
+ style={styles.modalOverlay}
120
+ onPress={() => setShowDeleteModal(false)}
121
+ >
122
+ <Pressable
123
+ style={styles.modalContent}
124
+ onPress={(e) => e.stopPropagation()}
125
+ >
126
+ <Text style={styles.modalTitle}>Delete Account</Text>
127
+
128
+ <View style={styles.warningContainer}>
129
+ <Text style={styles.warningTitle}>Warning</Text>
130
+ <Text style={styles.warningText}>
131
+ This action is permanent and cannot be undone. All your account data will be deleted immediately.
132
+ </Text>
133
+ </View>
134
+
135
+ <View style={styles.gdprNotice}>
136
+ <Text style={styles.gdprTitle}>Third-Party Data Notice</Text>
137
+ <Text style={styles.gdprText}>
138
+ Please note that data stored by third-party services (RevenueCat, Adjust, Scate) may persist on their servers.
139
+ </Text>
140
+ </View>
141
+
142
+ <Input
143
+ label="Type DELETE to confirm"
144
+ value={deleteConfirmText}
145
+ onChangeText={setDeleteConfirmText}
146
+ placeholder="DELETE"
147
+ autoCapitalize="characters"
148
+ containerStyle={styles.confirmInput}
149
+ />
150
+
151
+ <View style={styles.modalActions}>
152
+ <Button
153
+ title="Cancel"
154
+ variant="ghost"
155
+ onPress={() => {
156
+ setShowDeleteModal(false);
157
+ setDeleteConfirmText('');
158
+ }}
159
+ style={styles.modalButton}
160
+ />
161
+
162
+ <Button
163
+ title={isDeleting ? "Deleting..." : "Delete"}
164
+ variant="destructive"
165
+ onPress={handleDeleteAccount}
166
+ disabled={!isDeleteButtonEnabled}
167
+ loading={isDeleting}
168
+ style={styles.modalButton}
169
+ />
170
+ </View>
171
+ </Pressable>
172
+ </Pressable>
173
+ </KeyboardAvoidingView>
174
+ </Modal>
175
+ </ScrollView>
176
+ </SafeAreaView>
177
+ );
178
+ }
179
+
180
+ const createStyles = (theme: AppTheme) => StyleSheet.create({
181
+ safeArea: {
182
+ flex: 1,
183
+ backgroundColor: theme.colors.background,
184
+ },
185
+ header: {
186
+ flexDirection: 'row',
187
+ alignItems: 'center',
188
+ justifyContent: 'space-between',
189
+ paddingHorizontal: theme.spacing[4],
190
+ paddingVertical: theme.spacing[3],
191
+ backgroundColor: theme.colors.background,
192
+ },
193
+ backButton: {
194
+ padding: theme.spacing[2],
195
+ marginLeft: -theme.spacing[2],
196
+ },
197
+ headerTitle: {
198
+ fontSize: theme.typography.fontSize.lg,
199
+ fontWeight: '600',
200
+ color: theme.colors.text,
201
+ },
202
+ headerSpacer: {
203
+ width: 40,
204
+ },
205
+ container: {
206
+ flex: 1,
207
+ backgroundColor: theme.colors.background,
208
+ },
209
+ content: {
210
+ padding: theme.spacing[5],
211
+ },
212
+ userInfo: {
213
+ borderRadius: theme.borderRadius.xl,
214
+ padding: theme.spacing[5],
215
+ marginBottom: theme.spacing[8],
216
+ alignItems: 'center',
217
+ borderWidth: 1,
218
+ borderColor: theme.colors.borderLight,
219
+ },
220
+ greeting: {
221
+ fontSize: theme.typography.fontSize['xl'],
222
+ fontWeight: '700',
223
+ color: theme.colors.text,
224
+ marginBottom: theme.spacing[2],
225
+ },
226
+ email: {
227
+ fontSize: theme.typography.fontSize.base,
228
+ color: theme.colors.textSecondary,
229
+ marginBottom: theme.spacing[1],
230
+ },
231
+ userId: {
232
+ fontSize: theme.typography.fontSize.sm,
233
+ color: theme.colors.textMuted,
234
+ },
235
+ dangerZone: {
236
+ marginTop: theme.spacing[4],
237
+ padding: theme.spacing[4],
238
+ borderRadius: theme.borderRadius.lg,
239
+ backgroundColor: 'rgba(239, 68, 68, 0.05)',
240
+ borderWidth: 1,
241
+ borderColor: 'rgba(239, 68, 68, 0.2)',
242
+ },
243
+ dangerTitle: {
244
+ fontSize: theme.typography.fontSize.lg,
245
+ fontWeight: '600',
246
+ color: theme.colors.error,
247
+ marginBottom: theme.spacing[2],
248
+ },
249
+ dangerDescription: {
250
+ fontSize: theme.typography.fontSize.base,
251
+ color: theme.colors.textSecondary,
252
+ marginBottom: theme.spacing[4],
253
+ },
254
+ keyboardAvoidingModal: {
255
+ flex: 1,
256
+ },
257
+ modalOverlay: {
258
+ flex: 1,
259
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
260
+ justifyContent: 'center',
261
+ alignItems: 'center',
262
+ padding: theme.spacing[5],
263
+ },
264
+ modalContent: {
265
+ backgroundColor: theme.colors.background,
266
+ borderRadius: 24,
267
+ padding: theme.spacing[6],
268
+ paddingBottom: theme.spacing[12],
269
+ width: '100%',
270
+ maxWidth: 400,
271
+ borderWidth: 1,
272
+ borderColor: theme.colors.borderLight,
273
+ ...theme.shadows.large,
274
+ },
275
+ modalTitle: {
276
+ fontSize: theme.typography.fontSize['xl'],
277
+ fontWeight: 'bold',
278
+ color: theme.colors.text,
279
+ marginBottom: theme.spacing[6],
280
+ textAlign: 'center',
281
+ },
282
+ warningContainer: {
283
+ backgroundColor: 'transparent',
284
+ borderRadius: theme.borderRadius.lg,
285
+ padding: theme.spacing[4],
286
+ marginBottom: theme.spacing[5],
287
+ borderWidth: 1,
288
+ borderColor: theme.colors.warning,
289
+ borderStyle: 'dashed',
290
+ },
291
+ warningTitle: {
292
+ fontSize: theme.typography.fontSize.base,
293
+ fontWeight: '700',
294
+ color: theme.colors.warning,
295
+ marginBottom: theme.spacing[2],
296
+ },
297
+ warningText: {
298
+ fontSize: theme.typography.fontSize.sm,
299
+ color: theme.colors.textSecondary,
300
+ lineHeight: 20,
301
+ },
302
+ gdprNotice: {
303
+ backgroundColor: 'transparent',
304
+ borderRadius: theme.borderRadius.lg,
305
+ padding: theme.spacing[4],
306
+ marginBottom: theme.spacing[5],
307
+ borderWidth: 1,
308
+ borderColor: theme.colors.info,
309
+ },
310
+ gdprTitle: {
311
+ fontSize: theme.typography.fontSize.sm,
312
+ fontWeight: '600',
313
+ color: theme.colors.info,
314
+ marginBottom: theme.spacing[2],
315
+ },
316
+ gdprText: {
317
+ fontSize: theme.typography.fontSize.xs,
318
+ color: theme.colors.textMuted,
319
+ lineHeight: 18,
320
+ },
321
+ confirmInput: {
322
+ marginBottom: theme.spacing[6],
323
+ },
324
+ modalActions: {
325
+ flexDirection: 'row',
326
+ gap: theme.spacing[3],
327
+ },
328
+ modalButton: {
329
+ flex: 1,
330
+ },
331
+ });