eletrotupi / tcc/ commit / 6d8c0a7

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 files
Parents a0bf472
Changed 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 modified+11 -2
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
};