aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/(app)/(tabs)/_layout.tsx169
-rw-r--r--src/app/(app)/(tabs)/expenses.tsx2
-rw-r--r--src/app/(app)/(tabs)/index.tsx2
-rw-r--r--src/app/(app)/(tabs)/recurring.tsx2
-rw-r--r--src/app/(app)/(tabs)/salary.tsx2
-rw-r--r--src/app/(app)/_layout.tsx21
-rw-r--r--src/app/_layout.tsx54
-rw-r--r--src/app/biometrics.tsx120
-rw-r--r--src/app/login.tsx144
-rw-r--r--src/app/register.tsx142
10 files changed, 658 insertions, 0 deletions
diff --git a/src/app/(app)/(tabs)/_layout.tsx b/src/app/(app)/(tabs)/_layout.tsx
new file mode 100644
index 0000000..5033232
--- /dev/null
+++ b/src/app/(app)/(tabs)/_layout.tsx
@@ -0,0 +1,169 @@
+import React from 'react';
+import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
+import { Tabs } from 'expo-router';
+import { BlurView } from 'expo-blur';
+import { Feather } from '@expo/vector-icons';
+import Animated, {
+ useAnimatedStyle,
+ withSpring,
+ withTiming,
+} from 'react-native-reanimated';
+import * as Haptics from 'expo-haptics';
+import { Colors, Spacing } from '@/constants/theme';
+
+function TabBarIcon({
+ name,
+ isFocused,
+}: {
+ name: keyof typeof Feather.glyphMap;
+ isFocused: boolean;
+}) {
+ const animatedIconStyle = useAnimatedStyle(() => {
+ return {
+ transform: [
+ { scale: withSpring(isFocused ? 1.15 : 1, { damping: 12, stiffness: 300 }) },
+ ],
+ };
+ });
+
+ const animatedIndicatorStyle = useAnimatedStyle(() => {
+ return {
+ opacity: withTiming(isFocused ? 1 : 0, { duration: 200 }),
+ transform: [
+ { scale: withSpring(isFocused ? 1 : 0.5, { damping: 15, stiffness: 300 }) },
+ ],
+ };
+ });
+
+ return (
+ <View style={styles.iconContainer}>
+ <Animated.View style={animatedIconStyle}>
+ <Feather
+ name={name}
+ size={22}
+ color={isFocused ? Colors.primaryLight : Colors.textMuted}
+ />
+ </Animated.View>
+ <Animated.View style={[styles.indicator, animatedIndicatorStyle]} />
+ </View>
+ );
+}
+
+function CustomTabBar({ state, descriptors, navigation }: any) {
+ return (
+ <View style={styles.tabBarContainer}>
+ <BlurView
+ intensity={Platform.OS === 'ios' ? 80 : 100}
+ tint="dark"
+ style={styles.blurView}
+ >
+ {state.routes.map((route: any, index: number) => {
+ const { options } = descriptors[route.key];
+ const isFocused = state.index === index;
+
+ const onPress = () => {
+ const event = navigation.emit({
+ type: 'tabPress',
+ target: route.key,
+ canPreventDefault: true,
+ });
+
+ if (!isFocused && !event.defaultPrevented) {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ navigation.navigate(route.name);
+ }
+ };
+
+ let iconName: keyof typeof Feather.glyphMap = 'home';
+ if (route.name === 'index') iconName = 'pie-chart';
+ else if (route.name === 'salary') iconName = 'dollar-sign';
+ else if (route.name === 'recurring') iconName = 'repeat';
+ else if (route.name === 'expenses') iconName = 'plus-square';
+
+ return (
+ <TouchableOpacity
+ key={route.key}
+ accessibilityRole="button"
+ accessibilityState={isFocused ? { selected: true } : {}}
+ accessibilityLabel={options.tabBarAccessibilityLabel}
+ onPress={onPress}
+ style={styles.tabItem}
+ activeOpacity={1}
+ >
+ <TabBarIcon name={iconName} isFocused={isFocused} />
+ </TouchableOpacity>
+ );
+ })}
+ </BlurView>
+ </View>
+ );
+}
+
+export default function TabsLayout() {
+ return (
+ <Tabs
+ tabBar={(props) => <CustomTabBar {...props} />}
+ screenOptions={{
+ headerShown: false,
+ }}
+ >
+ <Tabs.Screen name="index" options={{ title: 'Dashboard' }} />
+ <Tabs.Screen name="salary" options={{ title: 'Salário' }} />
+ <Tabs.Screen name="recurring" options={{ title: 'Gastos Fixos' }} />
+ <Tabs.Screen name="expenses" options={{ title: 'Gastos do Mês' }} />
+ </Tabs>
+ );
+}
+
+const styles = StyleSheet.create({
+ tabBarContainer: {
+ position: 'absolute',
+ bottom: Platform.OS === 'ios' ? 30 : 20,
+ left: 24,
+ right: 24,
+ borderRadius: 32,
+ ...Platform.select({
+ ios: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 8 },
+ shadowOpacity: 0.5,
+ shadowRadius: 16,
+ },
+ android: {
+ elevation: 10,
+ },
+ }),
+ },
+ blurView: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-around',
+ borderRadius: 32,
+ backgroundColor: 'rgba(22, 27, 34, 0.75)',
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 0.08)',
+ overflow: 'hidden',
+ height: 64,
+ paddingHorizontal: Spacing.sm,
+ },
+ tabItem: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '100%',
+ },
+ iconContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: 44,
+ height: 44,
+ },
+ indicator: {
+ position: 'absolute',
+ bottom: -2,
+ width: 4,
+ height: 4,
+ borderRadius: 2,
+ backgroundColor: Colors.primaryLight,
+ },
+});
diff --git a/src/app/(app)/(tabs)/expenses.tsx b/src/app/(app)/(tabs)/expenses.tsx
new file mode 100644
index 0000000..c313dc6
--- /dev/null
+++ b/src/app/(app)/(tabs)/expenses.tsx
@@ -0,0 +1,2 @@
+import OneTimeExpensesScreen from '@/screens/one-time-expenses-screen';
+export default OneTimeExpensesScreen;
diff --git a/src/app/(app)/(tabs)/index.tsx b/src/app/(app)/(tabs)/index.tsx
new file mode 100644
index 0000000..73bede0
--- /dev/null
+++ b/src/app/(app)/(tabs)/index.tsx
@@ -0,0 +1,2 @@
+import DashboardScreen from '@/screens/dashboard-screen';
+export default DashboardScreen;
diff --git a/src/app/(app)/(tabs)/recurring.tsx b/src/app/(app)/(tabs)/recurring.tsx
new file mode 100644
index 0000000..c5340ed
--- /dev/null
+++ b/src/app/(app)/(tabs)/recurring.tsx
@@ -0,0 +1,2 @@
+import RecurringExpensesScreen from '@/screens/recurring-expenses-screen';
+export default RecurringExpensesScreen;
diff --git a/src/app/(app)/(tabs)/salary.tsx b/src/app/(app)/(tabs)/salary.tsx
new file mode 100644
index 0000000..1d30bb1
--- /dev/null
+++ b/src/app/(app)/(tabs)/salary.tsx
@@ -0,0 +1,2 @@
+import SalaryScreen from '@/screens/salary-screen';
+export default SalaryScreen;
diff --git a/src/app/(app)/_layout.tsx b/src/app/(app)/_layout.tsx
new file mode 100644
index 0000000..6099274
--- /dev/null
+++ b/src/app/(app)/_layout.tsx
@@ -0,0 +1,21 @@
+import { Colors } from "@/constants/theme";
+import { useAuth } from "@/contexts/AuthContext";
+import { Redirect, Slot } from "expo-router";
+import { ActivityIndicator, View } from "react-native";
+
+export default function AppLayout() {
+ const { user, isLoading } = useAuth();
+
+ if (isLoading) {
+ return (
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: Colors.background }}>
+ <ActivityIndicator size="large" color={Colors.primary} />
+ </View>
+ );
+ }
+
+ if (!user)
+ return <Redirect href="/login" />;
+
+ return <Slot />;
+} \ No newline at end of file
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
new file mode 100644
index 0000000..fe9f265
--- /dev/null
+++ b/src/app/_layout.tsx
@@ -0,0 +1,54 @@
+import { SplashScreen, Stack } from 'expo-router';
+import { useState, useEffect } from 'react';
+import { StatusBar } from 'expo-status-bar';
+import { SafeAreaProvider } from 'react-native-safe-area-context';
+import { useAppFonts } from '@/hooks/use-app-hooks';
+import { AuthProvider, useAuth } from '@/contexts/AuthContext';
+import { Colors } from '@/constants/theme';
+
+import { AnimatedSplashScreen } from '@/components/animated-splash-screen';
+import { View } from 'react-native';
+
+SplashScreen.preventAutoHideAsync();
+
+function RootContent() {
+ const { isLoading } = useAuth();
+ const { fontsLoaded, fontsError } = useAppFonts();
+ const [splashFinished, setSplashFinished] = useState(false);
+
+ const isAppReady = !isLoading;
+
+ useEffect(() => {
+ if (fontsLoaded || fontsError) {
+ SplashScreen.hideAsync();
+ }
+ }, [fontsLoaded, fontsError]);
+
+ if (!fontsLoaded && !fontsError)
+ return null;
+
+ return (
+ <View style={{ flex: 1, backgroundColor: Colors.background }}>
+ <Stack screenOptions={{ headerShown: false, contentStyle: { backgroundColor: Colors.background } }}>
+ <Stack.Screen name="(app)" />
+ <Stack.Screen name="login" />
+ <Stack.Screen name="register" />
+ </Stack>
+
+ {!splashFinished && (
+ <AnimatedSplashScreen isAppReady={isAppReady} onAnimationComplete={() => setSplashFinished(true)} />
+ )}
+ </View>
+ )
+}
+
+export default function RootLayout() {
+ return (
+ <SafeAreaProvider>
+ <StatusBar style="light" />
+ <AuthProvider>
+ <RootContent />
+ </AuthProvider>
+ </SafeAreaProvider>
+ )
+}
diff --git a/src/app/biometrics.tsx b/src/app/biometrics.tsx
new file mode 100644
index 0000000..500adc9
--- /dev/null
+++ b/src/app/biometrics.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import { useRouter } from 'expo-router';
+import * as SecureStore from 'expo-secure-store';
+import Animated, { FadeInDown, FadeInUp } from 'react-native-reanimated';
+import { Feather } from '@expo/vector-icons';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { Button } from '@/components/ui/button';
+import { Colors, Spacing, Typography, Radius } from '@/constants/theme';
+import { BlurView } from 'expo-blur';
+
+export default function BiometricsScreen() {
+ const router = useRouter();
+ const insets = useSafeAreaInsets();
+
+ async function handleAccept() {
+ await SecureStore.setItemAsync('yaba_use_biometrics', 'true');
+ router.replace('/');
+ }
+
+ function handleDecline() {
+ router.replace('/');
+ }
+
+ return (
+ <View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
+
+ <View style={styles.content}>
+ <Animated.View entering={FadeInDown.duration(600).delay(100)} style={styles.iconContainer}>
+ <View style={styles.iconBackground}>
+ <Feather size={64} color={Colors.primary} />
+ </View>
+ </Animated.View>
+
+ <Animated.View entering={FadeInUp.duration(600).delay(200)} style={styles.textContainer}>
+ <Text style={styles.title}>Login por Biometria</Text>
+ <Text style={styles.subtitle}>
+ Deseja utilizar sua biometria (Face ID / Digital) para acessar o app mais rapidamente nas próximas vezes?
+ </Text>
+ </Animated.View>
+ </View>
+
+ <Animated.View entering={FadeInUp.duration(600).delay(400)} style={styles.footer}>
+ <BlurView intensity={80} tint="dark" style={styles.blurContainer}>
+ <Button
+ label="Sim, usar biometria"
+ onPress={handleAccept}
+ fullWidth
+ style={styles.primaryButton}
+ />
+ <Button
+ label="Não, manter acesso livre"
+ variant="ghost"
+ onPress={handleDecline}
+ fullWidth
+ />
+ </BlurView>
+ </Animated.View>
+ </View>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.background,
+ justifyContent: 'space-between',
+ },
+ content: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: Spacing.xl,
+ gap: Spacing.xxl,
+ },
+ iconContainer: {
+ alignItems: 'center',
+ },
+ iconBackground: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ backgroundColor: 'rgba(238, 250, 13, 0.1)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderWidth: 1,
+ borderColor: 'rgba(238, 250, 13, 0.2)',
+ },
+ textContainer: {
+ alignItems: 'center',
+ gap: Spacing.sm,
+ },
+ title: {
+ ...Typography.h1,
+ color: Colors.textPrimary,
+ textAlign: 'center',
+ },
+ subtitle: {
+ ...Typography.bodyLarge,
+ color: Colors.textSecondary,
+ textAlign: 'center',
+ lineHeight: 24,
+ },
+ footer: {
+ paddingHorizontal: Spacing.lg,
+ paddingBottom: Spacing.lg,
+ },
+ blurContainer: {
+ padding: Spacing.lg,
+ borderRadius: Radius.lg,
+ gap: Spacing.md,
+ backgroundColor: 'rgba(22, 27, 34, 0.6)',
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 0.08)',
+ overflow: 'hidden',
+ },
+ primaryButton: {
+ height: 56,
+ }
+});
diff --git a/src/app/login.tsx b/src/app/login.tsx
new file mode 100644
index 0000000..15ec3be
--- /dev/null
+++ b/src/app/login.tsx
@@ -0,0 +1,144 @@
+import { useAuth } from "@/contexts/AuthContext";
+import { Link, useRouter } from "expo-router";
+import { useState } from "react";
+import {
+ Alert,
+ KeyboardAvoidingView,
+ Platform,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import * as LocalAuthentication from "expo-local-authentication";
+import Animated, { FadeInDown, FadeInUp } from "react-native-reanimated";
+import { Colors, Spacing, Typography } from "@/constants/theme";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+
+export default function LoginScreen() {
+ const insets = useSafeAreaInsets();
+ const router = useRouter();
+ const { login } = useAuth();
+
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+
+ async function handleLogin() {
+ if (!email || !password) {
+ Alert.alert("Erro", "Por favor, preencha todos os campos.");
+ return;
+ }
+
+ try {
+ setIsLoading(true);
+ await login({ email: email.trim(), password });
+
+ const hasHardware = await LocalAuthentication.hasHardwareAsync();
+ const isEnrolled = await LocalAuthentication.isEnrolledAsync();
+
+ if (hasHardware && isEnrolled) {
+ router.replace("/biometrics");
+ } else {
+ router.replace("/");
+ }
+ } catch (error) {
+ Alert.alert("Erro", "Falha ao fazer login. Verifique suas credenciais e tente novamente.");
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ return (
+ <KeyboardAvoidingView style={styles.container} behavior={Platform.OS === "ios" ? "padding" : undefined}>
+ <View style={[styles.content, { paddingTop: insets.top + Spacing.xxl }]}>
+ <Animated.View entering={FadeInUp.duration(600)} style={styles.header}>
+ <Text style={styles.title}>YABA</Text>
+ <Text style={styles.subtitle}>O seu dinheiro, no seu controle.</Text>
+ </Animated.View>
+
+ <Animated.View entering={FadeInDown.delay(200).duration(600)} style={styles.form}>
+ <Input
+ id="login-email"
+ label="E-mail"
+ placeholder="seu@email.com"
+ value={email}
+ onChangeText={setEmail}
+ keyboardType="email-address"
+ autoCapitalize="none"
+ />
+ <Input
+ id="login-password"
+ label="Senha"
+ placeholder="********"
+ value={password}
+ onChangeText={setPassword}
+ secureTextEntry
+ />
+
+ <Button
+ id="login-button"
+ label="Entrar"
+ fullWidth
+ onPress={handleLogin}
+ loading={isLoading}
+ disabled={isLoading}
+ />
+
+ <View style={styles.footer}>
+ <Text style={styles.footerText}>Não tem uma conta? </Text>
+ <Link href="/register" asChild>
+ <Text style={styles.linkText}>Cadastre-se</Text>
+ </Link>
+ </View>
+ </Animated.View>
+ </View>
+ </KeyboardAvoidingView>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.background,
+ },
+ content: {
+ flex: 1,
+ padding: Spacing.xl,
+ justifyContent: "center",
+ },
+ header: {
+ alignItems: "center",
+ marginBottom: Spacing.xl,
+ },
+ title: {
+ ...Typography.h1,
+ fontSize: 48,
+ color: Colors.primaryLight,
+ letterSpacing: 2,
+ fontFamily: "DMSans_700Bold",
+ },
+ subtitle: {
+ ...Typography.body,
+ color: Colors.textSecondary,
+ marginTop: Spacing.xs,
+ },
+ form: {
+ gap: Spacing.lg,
+ },
+ footer: {
+ flexDirection: "row",
+ justifyContent: "center",
+ marginTop: Spacing.xl,
+ },
+ footerText: {
+ ...Typography.body,
+ color: Colors.textSecondary,
+ },
+ linkText: {
+ ...Typography.body,
+ color: Colors.primaryLight,
+ fontFamily: "DMSans_700Bold",
+ },
+}); \ No newline at end of file
diff --git a/src/app/register.tsx b/src/app/register.tsx
new file mode 100644
index 0000000..aac588d
--- /dev/null
+++ b/src/app/register.tsx
@@ -0,0 +1,142 @@
+import React, { useState } from 'react';
+import { View, StyleSheet, Text, KeyboardAvoidingView, Platform, Alert, ScrollView } from 'react-native';
+import { Link, useRouter } from 'expo-router';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import Animated, { FadeInDown, FadeInUp } from 'react-native-reanimated';
+import * as LocalAuthentication from 'expo-local-authentication';
+import * as SecureStore from 'expo-secure-store';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Colors, Spacing, Typography } from '@/constants/theme';
+import { useAuth } from '@/contexts/AuthContext';
+
+export default function RegisterScreen() {
+ const insets = useSafeAreaInsets();
+ const router = useRouter();
+ const { register } = useAuth();
+
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ async function handleRegister() {
+ if (!name || !email || !password) {
+ Alert.alert('Erro', 'Preencha todos os campos.');
+ return;
+ }
+ try {
+ setIsLoading(true);
+ await register({ name: name.trim(), email: email.trim(), password });
+
+ const hasHardware = await LocalAuthentication.hasHardwareAsync();
+ const isEnrolled = await LocalAuthentication.isEnrolledAsync();
+
+ if (hasHardware && isEnrolled) {
+ router.replace('/biometrics');
+ } else {
+ router.replace('/');
+ }
+ } catch (err: any) {
+ Alert.alert('Erro no cadastro', 'Verifique seus dados.');
+ setIsLoading(false);
+ }
+ }
+
+ return (
+ <KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
+ <ScrollView contentContainerStyle={[styles.content, { paddingTop: insets.top + Spacing.xl }]}>
+ <Animated.View entering={FadeInUp.duration(600)} style={styles.header}>
+ <Text style={styles.title}>Criar Conta</Text>
+ <Text style={styles.subtitle}>Junte-se ao YABA e organize sua vida financeira.</Text>
+ </Animated.View>
+
+ <Animated.View entering={FadeInDown.delay(200).duration(600)} style={styles.form}>
+ <Input
+ id="register-name"
+ label="Nome"
+ placeholder="Como quer ser chamado?"
+ value={name}
+ onChangeText={setName}
+ />
+ <Input
+ id="register-email"
+ label="E-mail"
+ placeholder="seu@email.com"
+ value={email}
+ onChangeText={setEmail}
+ keyboardType="email-address"
+ autoCapitalize="none"
+ />
+ <Input
+ id="register-password"
+ label="Senha"
+ placeholder="No mínimo 6 caracteres"
+ value={password}
+ onChangeText={setPassword}
+ secureTextEntry
+ />
+
+ <Button
+ id="register-button"
+ label="Cadastrar"
+ fullWidth
+ onPress={handleRegister}
+ loading={isLoading}
+ disabled={isLoading}
+ />
+
+ <View style={styles.footer}>
+ <Text style={styles.footerText}>Já tem uma conta? </Text>
+ <Link href="/login" asChild>
+ <Text style={styles.linkText}>Faça login</Text>
+ </Link>
+ </View>
+ </Animated.View>
+ </ScrollView>
+ </KeyboardAvoidingView>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.background,
+ },
+ content: {
+ flexGrow: 1,
+ padding: Spacing.xl,
+ justifyContent: 'center',
+ },
+ header: {
+ alignItems: 'center',
+ marginBottom: Spacing.xl,
+ },
+ title: {
+ ...Typography.h1,
+ color: Colors.textPrimary,
+ },
+ subtitle: {
+ ...Typography.body,
+ color: Colors.textSecondary,
+ marginTop: Spacing.xs,
+ textAlign: 'center',
+ },
+ form: {
+ gap: Spacing.lg,
+ },
+ footer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginTop: Spacing.xl,
+ },
+ footerText: {
+ ...Typography.body,
+ color: Colors.textSecondary,
+ },
+ linkText: {
+ ...Typography.body,
+ color: Colors.primaryLight,
+ fontFamily: 'DMSans_700Bold',
+ },
+});