frontend: a very early draft activate account
Plenty to-do: 1. Split each digit into a single, smaller field 2. Jump focus between fields 3. Rig "resend code" button (requires a API patch) 4. Display a small timer
Parents:
a0bf4728 file(s) changed
- frontend/app/auth/_layout.tsx +7 -0
- frontend/app/auth/activate.tsx +159 -0
- frontend/app/auth/login.tsx +4 -3
- frontend/app/auth/signup.tsx +2 -2
- frontend/app/index.tsx +11 -2
- frontend/context/AuthContext.tsx +17 -3
- frontend/lib/api/client.ts +12 -2
- frontend/lib/api/types.ts +5 -0
frontend/app/auth/_layout.tsx
@@ -10,6 +10,12 @@ headerShown: false,
10 10 }}
11 11 />
12 12 <Stack.Screen
13 + name="activate"
14 + options={{
15 + headerShown: false,
16 + }}
17 + />
18 + <Stack.Screen
13 19 name="signup"
14 20 options={{
15 21 headerShown: false,
@@ -29,4 +35,4 @@ }}
29 35 />
30 36 </Stack>
31 37 );
32 - }
38 + }
frontend/app/auth/activate.tsx
@@ -0,0 +1,159 @@
1 + import React, { useState } from 'react';
2 + import {
3 + View,
4 + Text,
5 + StyleSheet,
6 + ScrollView,
7 + KeyboardAvoidingView,
8 + Platform,
9 + Alert,
10 + } from 'react-native';
11 + import { Link, useLocalSearchParams, router } from 'expo-router';
12 + import { SafeAreaView } from 'react-native-safe-area-context';
13 + import { useThemeColor } from '@/hooks/use-theme-color';
14 + import { Button, Input } from '@/components/ui';
15 + import { Colors } from '@/constants/theme'
16 + import { useAuth } from '@/context/AuthContext';
17 +
18 + export default function ActivateScreen() {
19 + const { userId } = useLocalSearchParams();
20 + const [code, setCode] = useState('');
21 + const [errors, setErrors] = useState<{
22 + code?: string;
23 + }>({});
24 +
25 + const { activate } = useAuth();
26 + const [loading, setLoading] = useState(false);
27 + const textColor = useThemeColor({}, 'text');
28 + const backgroundColor = useThemeColor({}, 'background');
29 + const tintColor = useThemeColor({}, 'tint');
30 +
31 + const validateForm = () => {
32 + const newErrors: typeof errors = {};
33 +
34 + if (!code) {
35 + newErrors.code = 'O código é obrigatório';
36 + } else if (code.length < 6) {
37 + newErrors.code = 'O código deve conter pelo menos 6 caracteres';
38 + }
39 +
40 + setErrors(newErrors);
41 +
42 + return Object.keys(newErrors).length === 0;
43 + };
44 +
45 + const handleActivate = async () => {
46 + if (!validateForm()) return;
47 +
48 + setLoading(true);
49 + try {
50 + await activate(Number(code.replaceAll(' ', '')));
51 +
52 + Alert.alert('Sucesso!', 'Conta ativada com sucesso', [
53 + {
54 + text: 'OK',
55 + onPress: () => router.replace('/'),
56 + },
57 + ]);
58 + } catch (error: any) {
59 + Alert.alert('Erro: ', error.message || 'Falha em ativar a conta');
60 + } finally {
61 + setLoading(false);
62 + }
63 + };
64 +
65 + const resendCode = async () => {
66 + }
67 +
68 + return (
69 + <SafeAreaView style={[styles.container, { backgroundColor }]}>
70 + <KeyboardAvoidingView
71 + behavior='height'
72 + style={styles.keyboardView}
73 + >
74 + <ScrollView
75 + contentContainerStyle={styles.scrollContainer}
76 + showsVerticalScrollIndicator={false}
77 + >
78 + <View style={styles.header}>
79 + <Text style={[styles.title, { color: textColor }]}>Ativar a conta</Text>
80 + <Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
81 + Digite o código de ativação enviado para seu email
82 + </Text>
83 + </View>
84 +
85 + <View style={styles.form}>
86 + <Input
87 + label="Código"
88 + type="text"
89 + value={code}
90 + onChangeText={setCode}
91 + keyboardType="number-pad"
92 + placeholder="Digite o código enviado para sua conta"
93 + error={errors.code}
94 + />
95 +
96 + <Button
97 + title="Ativar conta"
98 + onPress={handleActivate}
99 + loading={loading}
100 + style={styles.resetButton}
101 + />
102 + </View>
103 +
104 + <View style={styles.footer}>
105 + <Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
106 + Não recebeu?{' '}
107 + </Text>
108 +
109 + <Button variant="outline" title="Reenviar um novo código" onPressed={resendCode} />
110 + </View>
111 + </ScrollView>
112 + </KeyboardAvoidingView>
113 + </SafeAreaView>
114 + );
115 + }
116 +
117 + const styles = StyleSheet.create({
118 + container: {
119 + flex: 1,
120 + },
121 + keyboardView: {
122 + flex: 1,
123 + },
124 + scrollContainer: {
125 + flexGrow: 1,
126 + paddingHorizontal: 24,
127 + justifyContent: 'center',
128 + minHeight: '100%',
129 + },
130 + header: {
131 + alignItems: 'center',
132 + marginBottom: 40,
133 + },
134 + title: {
135 + fontSize: 28,
136 + fontWeight: 'bold',
137 + marginBottom: 8,
138 + },
139 + subtitle: {
140 + fontSize: 16,
141 + textAlign: 'center',
142 + },
143 + form: {
144 + marginBottom: 32,
145 + },
146 + resetButton: {
147 + marginTop: 8,
148 + },
149 + footer: {
150 + marginTop: 20,
151 + borderTopColor: Colors.light.accentBlue,
152 + borderTopWidth: 1,
153 + paddingTop: 20
154 + },
155 + footerText: {
156 + textAlign: 'center',
157 + marginBottom: 10
158 + }
159 + });
frontend/app/auth/login.tsx
@@ -47,9 +47,10 @@ if (!validateForm()) return;
47 47
48 48 try {
49 49 await login(email, password);
50 - router.replace('/(tabs)/new');
50 + router.replace('/');
51 51 } catch (error: any) {
52 - Alert.alert('Erro: ', 'Falha ao fazer login');
52 + console.log("LOGIN error: ", error)
53 + Alert.alert('Erro: ', 'Falha ao fazer login');
53 54 }
54 55 };
55 56
@@ -165,7 +166,7 @@ flexDirection: 'row',
165 166 justifyContent: 'center',
166 167 alignItems: 'center',
167 168 marginTop: 'auto',
168 - paddingBottom: 20,
169 + paddingBottom: 40,
169 170 },
170 171 footerText: {
171 172 fontSize: 14,
frontend/app/auth/signup.tsx
@@ -122,11 +122,11 @@ autoCapitalize="none"
122 122 />
123 123
124 124 <Input
125 - label="Password"
125 + label="Senha"
126 126 type="password"
127 127 value={password}
128 128 onChangeText={setPassword}
129 - placeholder="Escolha uma senha (min. 6 caracteres)"
129 + placeholder="Deve ter no min. 6 caracteres"
130 130 error={errors.password}
131 131 showPasswordToggle
132 132 />
frontend/app/index.tsx
@@ -3,7 +3,8 @@ import { useAuth } from '@/context/AuthContext';
3 3 import { ActivityIndicator, View } from 'react-native';
4 4
5 5 export default function Index() {
6 - const { isAuthenticated, isLoading } = useAuth();
6 + const { isAuthenticated, isLoading, user } = useAuth();
7 + let url = "/auth/login";
7 8
8 9 if (isLoading) {
9 10 return (
@@ -13,5 +14,13 @@ </View>
13 14 );
14 15 }
15 16
16 - return <Redirect href={isAuthenticated ? '/(tabs)/new' : '/auth/login'} />;
17 + if (isAuthenticated) {
18 + if (user.active) {
19 + url = "/(tabs)/new"
20 + } else {
21 + url = "/auth/activate"
22 + }
23 + }
24 +
25 + return <Redirect href={url} />;
17 26 }
frontend/context/AuthContext.tsx
@@ -17,6 +17,7 @@ email: string;
17 17 updatedAt: date;
18 18 avatarURL?: string;
19 19 avatarKey?: string;
20 + active: boolean;
20 21 }) => void;
21 22 login: (email: string, password: string) => Promise<void>;
22 23 signup: (userData: {
@@ -28,9 +29,9 @@ }) => Promise<void>;
28 29 logout: () => Promise<void>;
29 30 forgotPassword: (email: string) => Promise<{ message: string; token?: string }>;
30 31 resetPassword: (token: string, password: string) => Promise<void>;
32 + activate: (code: string) => Promise<void>;
31 33 }
32 34
33 -
34 35 export function sanitizeUser(data: any): User {
35 36 return {
36 37 id: data.id,
@@ -40,6 +41,7 @@ lastName: data.lastName,
40 41 updatedAt: data.updatedAt,
41 42 avatarKey: data.avatarKey,
42 43 avatarURL: data.avatarURL,
44 + active: data.active
43 45 };
44 46 }
45 47
@@ -79,7 +81,8 @@ email: verifyResponse.email,
79 81 firstName: verifyResponse.user.firstName,
80 82 lastName: verifyResponse.user.lastName,
81 83 avatarURL: verifyResponse.user.avatarURL,
82 - avatarKey: verifyResponse.user.avatarKey
84 + avatarKey: verifyResponse.user.avatarKey,
85 + active: verifyResponse.user.active
83 86 });
84 87 }
85 88 }
@@ -140,6 +143,16 @@ const resetPassword = async (token: string, password: string) => {
140 143 await apiClient.resetPassword(token, password);
141 144 };
142 145
146 + const activate = async (code: number) => {
147 + const response = await apiClient.activate(code);
148 +
149 + if (response.user) {
150 + updateAuthUser(response.user)
151 + }
152 +
153 + return response
154 + }
155 +
143 156 const updateAuthUser = (user) => {
144 157 setUser(sanitizeUser(user));
145 158 }
@@ -153,7 +166,8 @@ signup,
153 166 logout,
154 167 forgotPassword,
155 168 resetPassword,
156 - updateAuthUser
169 + updateAuthUser,
170 + activate
157 171 };
158 172
159 173 return (
frontend/lib/api/client.ts
@@ -80,14 +80,24 @@ return response.json();
80 80 }
81 81
82 82 // Auth
83 - async login(email: string, password: string): Promise<AuthResponse> {
83 + async login(email: string, password: string): Promise<AuthResponse | { isActive: boolean }> {
84 84 const response = await this.request<AuthResponse>('/auth/login', {
85 85 method: 'POST',
86 86 body: JSON.stringify({ email, password }),
87 87 });
88 88
89 - await this.storeToken(response.token);
89 + if (response.token) {
90 + await this.storeToken(response.token);
91 + }
92 +
90 93 return response;
94 + }
95 +
96 + async activate(code: number): Promise<ActivateResponse> {
97 + return this.request('/auth/activate', {
98 + method: 'POST',
99 + body: JSON.stringify({ code }),
100 + });
91 101 }
92 102
93 103 async signup(user: SignUpPayload): Promise<AuthResponse> {
frontend/lib/api/types.ts
@@ -6,6 +6,7 @@ lastName?: string;
6 6 updatedAt: Date;
7 7 avatarKey?: string;
8 8 avatarURL?: string;
9 + active: boolean;
9 10 };
10 11
11 12 // Auth
@@ -36,6 +37,10 @@
36 37 export interface ResetPasswordResponse {
37 38 message: string;
38 39 };
40 +
41 + export interface ActivateResponse {
42 + activated: boolean;
43 + }
39 44
40 45 // User
41 46 export interface UserUpdatePayload extends Partial<User> {