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
Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago
6d8c0a74220b36e86fd53538617fd70ce2618f59 Browse filesParents
a0bf472Changed files
8
Additions
+217
Deletions
-12
frontend/app/auth/_layout.tsx
1 1
import { Stack } from 'expo-router';
2 2
3 3
export default function AuthLayout() {
4 4
return (
5 5
<Stack>
8 8
options={{
9 9
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,
16 22
}}
17 23
/>
27 33
headerShown: false,
28 34
}}
29 35
/>
30 36
</Stack>
31 37
);
32 -
} 38 +
}
/dev/null → frontend/app/auth/activate.tsx
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
1 1
import React, { useState } from 'react';
2 2
import {
3 3
View,
4 4
Text,
5 5
StyleSheet,
45 45
const handleLogin = async () => {
46 46
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
56 57
return (
57 58
<SafeAreaView style={[styles.container, { backgroundColor }]}>
163 164
footer: {
164 165
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,
172 173
},
173 174
});
frontend/app/auth/signup.tsx
1 1
import React, { useState } from 'react';
2 2
import {
3 3
View,
4 4
Text,
5 5
StyleSheet,
120 120
error={errors.email}
121 121
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
/>
133 133
134 134
<Input
211 211
},
212 212
footerText: {
213 213
fontSize: 14,
214 214
},
215 215
});
frontend/app/index.tsx
1 1
import { Redirect } from 'expo-router';
2 2
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 (
10 11
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
11 12
<ActivityIndicator size="large" />
12 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
1 1
import React, { createContext, useContext, useEffect, useState } from 'react';
2 2
import { apiClient, User } from '@/lib/api';
3 3
import { queryClient, storage } from '@/lib/queryClient';
4 4
5 5
interface AuthProviderProps {
15 15
lastName?: string;
16 16
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: {
23 24
firstName: string;
24 25
lastName?: string;
26 27
password: string;
27 28
}) => 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,
37 38
email: data.email,
38 39
firstName: data.firstName,
39 40
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
46 48
const AuthContext = createContext<AuthContextType | null>(null);
47 49
77 79
id: verifyResponse.userId,
78 80
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
}
86 89
} catch (error) {
87 90
console.error('Auth check failed:', error);
138 141
139 142
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
}
146 159
147 160
const value: AuthContextType = {
151 164
login,
152 165
signup,
153 166
logout,
154 167
forgotPassword,
155 168
resetPassword,
156 -
updateAuthUser
169 +
updateAuthUser,
170 +
activate
157 171
};
158 172
159 173
return (
160 174
<AuthContext.Provider value={value}>
161 175
{children}
162 176
</AuthContext.Provider>
163 177
);
164 178
}
frontend/lib/api/client.ts
1 1
import * as SecureStore from 'expo-secure-store';
2 2
import {
3 3
User,
4 4
AuthResponse,
5 5
VerifyTokenResponse,
78 78
79 79
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> {
94 104
const response = await this.request<AuthResponse>('/users', {
95 105
method: 'POST',
208 218
return this.request(`/sleep_records${qs ? `?${qs}` : ''}`);
209 219
}
210 220
}
211 221
212 222
export const apiClient = new ApiClient();
frontend/lib/api/types.ts
1 1
export interface User {
2 2
id: number;
3 3
email: string;
4 4
firstName: string;
5 5
lastName?: string;
6 6
updatedAt: Date;
7 7
avatarKey?: string;
8 8
avatarURL?: string;
9 +
active: boolean;
9 10
};
10 11
11 12
// Auth
12 13
export interface AuthResponse {
13 14
token: string;
34 35
};
35 36
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> {
42 47
id: number;
43 48
}
105 110
entries: T[];
106 111
total: number;
107 112
page: number;
108 113
nextPage: number | null;
109 114
};