added login and register
This commit is contained in:
66
src/context/AuthContext.tsx
Normal file
66
src/context/AuthContext.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import { User, LoginRequest, RegisterRequest } from '../types';
|
||||
import { authService } from '../services/auth.service';
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
isLoading: boolean;
|
||||
signIn: (credentials: LoginRequest) => Promise<void>;
|
||||
signUp: (data: RegisterRequest) => Promise<void>;
|
||||
signOut: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const signIn = async (credentials: LoginRequest) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await authService.login(credentials);
|
||||
setToken(response.access_token);
|
||||
setUser(response.user);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const signUp = async (data: RegisterRequest) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await authService.register(data);
|
||||
// After successful registration, sign in automatically
|
||||
await signIn({ username: data.username, password: data.password });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const signOut = () => {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, token, isLoading, signIn, signUp, signOut }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
20
src/navigation/AuthNavigator.tsx
Normal file
20
src/navigation/AuthNavigator.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import LoginScreen from '../screens/LoginScreen';
|
||||
import RegisterScreen from '../screens/RegisterScreen';
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
|
||||
export default function AuthNavigator() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="Login" component={LoginScreen} />
|
||||
<Stack.Screen name="Register" component={RegisterScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
256
src/screens/LoginScreen.tsx
Normal file
256
src/screens/LoginScreen.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, borderRadius, typography, shadows } from '../theme/colors';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
export default function LoginScreen({ navigation }: any) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { signIn, isLoading } = useAuth();
|
||||
|
||||
const handleLogin = async () => {
|
||||
setError(null);
|
||||
if (!email || !password) {
|
||||
setError('Please enter both username and password.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await signIn({ username: email, password });
|
||||
} catch (err: any) {
|
||||
setError('Login failed. Please check your credentials.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.content}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.logoContainer}>
|
||||
<MaterialCommunityIcons name="anchor" size={48} color={colors.sentinel.primary} />
|
||||
</View>
|
||||
<Text style={styles.title}>Welcome Aboard!</Text>
|
||||
<Text style={styles.subtitle}>Sign in to access your sanctuaryy</Text>
|
||||
{error && (
|
||||
<View style={styles.errorContainer}>
|
||||
<MaterialCommunityIcons name="alert-circle-outline" size={20} color={colors.sentinel.statusCritical} />
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Feather name="mail" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Username"
|
||||
placeholderTextColor={colors.sentinel.textSecondary}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Feather name="lock" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
placeholderTextColor={colors.sentinel.textSecondary}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.forgotButton}>
|
||||
<Text style={styles.forgotText}>Forgot Password?</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.loginButton}
|
||||
activeOpacity={0.8}
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={colors.white} />
|
||||
) : (
|
||||
<Text style={styles.loginButtonText}>Login</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.divider}>
|
||||
<View style={styles.dividerLine} />
|
||||
<Text style={styles.dividerText}>OR</Text>
|
||||
<View style={styles.dividerLine} />
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.registerLink}
|
||||
onPress={() => navigation.navigate('Register')}
|
||||
>
|
||||
<Text style={styles.registerText}>
|
||||
Don't have an account? <Text style={styles.registerHighlight}>Join the Fleet</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: spacing.lg,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.xxl,
|
||||
},
|
||||
logoContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: borderRadius.full,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(184, 224, 229, 0.2)',
|
||||
},
|
||||
title: {
|
||||
fontSize: typography.fontSize.xxl,
|
||||
fontWeight: 'bold',
|
||||
color: colors.sentinel.text,
|
||||
marginBottom: spacing.xs,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.sentinel.textSecondary,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
errorContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 68, 68, 0.1)',
|
||||
padding: spacing.sm,
|
||||
borderRadius: borderRadius.md,
|
||||
marginTop: spacing.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 68, 68, 0.2)',
|
||||
},
|
||||
errorText: {
|
||||
color: colors.sentinel.statusCritical,
|
||||
fontSize: typography.fontSize.sm,
|
||||
marginLeft: spacing.xs,
|
||||
fontWeight: '500',
|
||||
},
|
||||
form: {
|
||||
width: '100%',
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(184, 224, 229, 0.1)',
|
||||
marginBottom: spacing.base,
|
||||
height: 56,
|
||||
paddingHorizontal: spacing.base,
|
||||
},
|
||||
inputIcon: {
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
color: colors.sentinel.text,
|
||||
fontSize: typography.fontSize.base,
|
||||
},
|
||||
forgotButton: {
|
||||
alignSelf: 'flex-end',
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
forgotText: {
|
||||
color: colors.sentinel.primary,
|
||||
fontSize: typography.fontSize.sm,
|
||||
},
|
||||
loginButton: {
|
||||
backgroundColor: colors.nautical.teal,
|
||||
height: 56,
|
||||
borderRadius: borderRadius.lg,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...shadows.glow,
|
||||
},
|
||||
loginButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: typography.fontSize.md,
|
||||
fontWeight: 'bold',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
divider: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginVertical: spacing.xl,
|
||||
},
|
||||
dividerLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: 'rgba(184, 224, 229, 0.1)',
|
||||
},
|
||||
dividerText: {
|
||||
color: colors.sentinel.textSecondary,
|
||||
paddingHorizontal: spacing.md,
|
||||
fontSize: typography.fontSize.sm,
|
||||
},
|
||||
registerLink: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
registerText: {
|
||||
color: colors.sentinel.textSecondary,
|
||||
fontSize: typography.fontSize.base,
|
||||
},
|
||||
registerHighlight: {
|
||||
color: colors.sentinel.primary,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
// Sentinel Protocol Status
|
||||
const protocolStatus = {
|
||||
@@ -179,21 +180,22 @@ export default function MeScreen() {
|
||||
const [triggerGraceDays, setTriggerGraceDays] = useState(15);
|
||||
const [triggerSource, setTriggerSource] = useState<'dual' | 'subscription' | 'activity'>('dual');
|
||||
const [triggerKillSwitch, setTriggerKillSwitch] = useState(true);
|
||||
const { user, signOut } = useAuth();
|
||||
|
||||
const handleOpenLink = (url: string) => {
|
||||
Linking.openURL(url).catch(() => {});
|
||||
Linking.openURL(url).catch(() => { });
|
||||
};
|
||||
|
||||
const handleAbandonIsland = () => {
|
||||
Alert.alert(
|
||||
'Abandon Island',
|
||||
'Are you sure you want to delete your account? This action is irreversible. All your data, including vault contents, will be permanently destroyed.',
|
||||
'Sign Out',
|
||||
'Are you sure you want to sign out?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Delete Account',
|
||||
{
|
||||
text: 'Sign Out',
|
||||
style: 'destructive',
|
||||
onPress: () => Alert.alert('Account Deletion', 'Please contact support to proceed with account deletion.')
|
||||
onPress: signOut
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -206,7 +208,7 @@ export default function MeScreen() {
|
||||
style={styles.gradient}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ScrollView
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
@@ -235,11 +237,11 @@ export default function MeScreen() {
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.profileInfo}>
|
||||
<Text style={styles.profileName}>Captain</Text>
|
||||
<Text style={styles.profileName}>{user?.username || 'Captain'}</Text>
|
||||
<Text style={styles.profileTitle}>MASTER OF THE SANCTUM</Text>
|
||||
<View style={styles.profileBadge}>
|
||||
<MaterialCommunityIcons name="crown" size={12} color={colors.nautical.gold} />
|
||||
<Text style={styles.profileBadgeText}>Pro Member</Text>
|
||||
<Text style={styles.profileBadgeText}>{user?.tier || 'Pro Member'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -280,8 +282,8 @@ export default function MeScreen() {
|
||||
<Text style={styles.sectionTitle}>THE CAPTAIN'S PROTOCOLS</Text>
|
||||
<View style={styles.menuCard}>
|
||||
{captainProtocols.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={[
|
||||
styles.menuItem,
|
||||
index < captainProtocols.length - 1 && styles.menuItemBorder
|
||||
@@ -309,22 +311,22 @@ export default function MeScreen() {
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Abandon Island Button */}
|
||||
<TouchableOpacity
|
||||
{/* Abandon Island Button (Logout for now) */}
|
||||
<TouchableOpacity
|
||||
style={styles.abandonButton}
|
||||
onPress={handleAbandonIsland}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Feather name="log-out" size={18} color={colors.nautical.coral} />
|
||||
<Text style={styles.abandonButtonText}>ABANDON ISLAND</Text>
|
||||
<Text style={styles.abandonButtonText}>SIGN OUT</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Settings Menu */}
|
||||
<Text style={styles.sectionTitle}>SETTINGS</Text>
|
||||
<View style={styles.menuCard}>
|
||||
{settingsMenu.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={[
|
||||
styles.menuItem,
|
||||
index < settingsMenu.length - 1 && styles.menuItemBorder
|
||||
@@ -351,7 +353,7 @@ export default function MeScreen() {
|
||||
<Text style={styles.sectionTitle}>ABOUT</Text>
|
||||
<View style={styles.aboutGrid}>
|
||||
{protocolExplainers.slice(0, 2).map((item) => (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={styles.aboutCard}
|
||||
onPress={() => setSelectedExplainer(item)}
|
||||
@@ -407,7 +409,7 @@ export default function MeScreen() {
|
||||
</View>
|
||||
<Text style={styles.modalTitle}>{selectedExplainer.title}</Text>
|
||||
</View>
|
||||
<ScrollView
|
||||
<ScrollView
|
||||
style={styles.modalScroll}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
@@ -443,11 +445,11 @@ export default function MeScreen() {
|
||||
</View>
|
||||
<ScrollView style={styles.spiritScroll} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.mnemonicCard}>
|
||||
<View style={styles.mnemonicHeader}>
|
||||
<View style={styles.mnemonicHeaderText}>
|
||||
<Text style={styles.mnemonicSubtitle}>12 words split into two sealed shards</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.mnemonicHeader}>
|
||||
<View style={styles.mnemonicHeaderText}>
|
||||
<Text style={styles.mnemonicSubtitle}>12 words split into two sealed shards</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.mnemonicBody}>
|
||||
<View style={styles.mnemonicShard}>
|
||||
@@ -661,126 +663,126 @@ export default function MeScreen() {
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowTideModal(false)}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
|
||||
<TouchableOpacity activeOpacity={1} onPress={() => { }}>
|
||||
<View style={styles.spiritModal}>
|
||||
<View style={styles.spiritHeader}>
|
||||
<View style={styles.spiritIcon}>
|
||||
<MaterialCommunityIcons name="weather-cloudy" size={24} color={colors.me.primary} />
|
||||
</View>
|
||||
<Text style={styles.spiritTitle}>Tide Notifications</Text>
|
||||
</View>
|
||||
<ScrollView style={styles.spiritScroll} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.tideSection}>
|
||||
<Text style={styles.tideLabel}>ALERT LEVEL</Text>
|
||||
<View style={styles.tideRow}>
|
||||
{[
|
||||
{ key: 'low', label: 'Low Tide' },
|
||||
{ key: 'high', label: 'High Tide' },
|
||||
{ key: 'red', label: 'Red Tide' },
|
||||
].map((item) => {
|
||||
const isActive = tideLevel === item.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={item.key}
|
||||
style={[styles.tideButton, isActive && styles.tideButtonActive]}
|
||||
onPress={() => setTideLevel(item.key as typeof tideLevel)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[styles.tideButtonText, isActive && styles.tideButtonTextActive]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
<View style={styles.spiritHeader}>
|
||||
<View style={styles.spiritIcon}>
|
||||
<MaterialCommunityIcons name="weather-cloudy" size={24} color={colors.me.primary} />
|
||||
</View>
|
||||
<Text style={styles.spiritTitle}>Tide Notifications</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.tideSection}>
|
||||
<Text style={styles.tideLabel}>CHANNELS</Text>
|
||||
<View style={styles.channelGrid}>
|
||||
{[
|
||||
{ key: 'push', label: 'App Push', icon: 'notifications' },
|
||||
{ key: 'email', label: 'Email', icon: 'mail' },
|
||||
{ key: 'sms', label: 'SMS', icon: 'chatbubble' },
|
||||
{ key: 'emergency', label: 'Emergency', icon: 'alert' },
|
||||
].map((item) => {
|
||||
const isActive = tideChannels[item.key as keyof typeof tideChannels];
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={item.key}
|
||||
style={[styles.channelCard, isActive && styles.channelCardActive]}
|
||||
onPress={() =>
|
||||
setTideChannels((prev) => ({
|
||||
...prev,
|
||||
[item.key]: !prev[item.key as keyof typeof prev],
|
||||
}))
|
||||
}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon as any}
|
||||
size={18}
|
||||
color={isActive ? colors.me.primary : colors.me.textSecondary}
|
||||
/>
|
||||
<Text style={[styles.channelText, isActive && styles.channelTextActive]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
<ScrollView style={styles.spiritScroll} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.tideSection}>
|
||||
<Text style={styles.tideLabel}>ALERT LEVEL</Text>
|
||||
<View style={styles.tideRow}>
|
||||
{[
|
||||
{ key: 'low', label: 'Low Tide' },
|
||||
{ key: 'high', label: 'High Tide' },
|
||||
{ key: 'red', label: 'Red Tide' },
|
||||
].map((item) => {
|
||||
const isActive = tideLevel === item.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={item.key}
|
||||
style={[styles.tideButton, isActive && styles.tideButtonActive]}
|
||||
onPress={() => setTideLevel(item.key as typeof tideLevel)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[styles.tideButtonText, isActive && styles.tideButtonTextActive]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.tideSection}>
|
||||
<Text style={styles.tideLabel}>CADENCE</Text>
|
||||
<View style={styles.tideRow}>
|
||||
{[
|
||||
{ key: 'daily', label: 'Daily' },
|
||||
{ key: 'weekly', label: 'Weekly' },
|
||||
{ key: 'monthly', label: 'Monthly' },
|
||||
].map((item) => {
|
||||
const isActive = tideCadence === item.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={item.key}
|
||||
style={[styles.tideButton, isActive && styles.tideButtonActive]}
|
||||
onPress={() => setTideCadence(item.key as typeof tideCadence)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[styles.tideButtonText, isActive && styles.tideButtonTextActive]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
<View style={styles.tideSection}>
|
||||
<Text style={styles.tideLabel}>CHANNELS</Text>
|
||||
<View style={styles.channelGrid}>
|
||||
{[
|
||||
{ key: 'push', label: 'App Push', icon: 'notifications' },
|
||||
{ key: 'email', label: 'Email', icon: 'mail' },
|
||||
{ key: 'sms', label: 'SMS', icon: 'chatbubble' },
|
||||
{ key: 'emergency', label: 'Emergency', icon: 'alert' },
|
||||
].map((item) => {
|
||||
const isActive = tideChannels[item.key as keyof typeof tideChannels];
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={item.key}
|
||||
style={[styles.channelCard, isActive && styles.channelCardActive]}
|
||||
onPress={() =>
|
||||
setTideChannels((prev) => ({
|
||||
...prev,
|
||||
[item.key]: !prev[item.key as keyof typeof prev],
|
||||
}))
|
||||
}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon as any}
|
||||
size={18}
|
||||
color={isActive ? colors.me.primary : colors.me.textSecondary}
|
||||
/>
|
||||
<Text style={[styles.channelText, isActive && styles.channelTextActive]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.tideHint}>
|
||||
<Ionicons name="information-circle" size={16} color={colors.me.textSecondary} />
|
||||
<Text style={styles.tideHintText}>
|
||||
Last confirmation: 3 days ago · Next reminder in 4 days.
|
||||
</Text>
|
||||
<View style={styles.tideSection}>
|
||||
<Text style={styles.tideLabel}>CADENCE</Text>
|
||||
<View style={styles.tideRow}>
|
||||
{[
|
||||
{ key: 'daily', label: 'Daily' },
|
||||
{ key: 'weekly', label: 'Weekly' },
|
||||
{ key: 'monthly', label: 'Monthly' },
|
||||
].map((item) => {
|
||||
const isActive = tideCadence === item.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={item.key}
|
||||
style={[styles.tideButton, isActive && styles.tideButtonActive]}
|
||||
onPress={() => setTideCadence(item.key as typeof tideCadence)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[styles.tideButtonText, isActive && styles.tideButtonTextActive]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.tideHint}>
|
||||
<Ionicons name="information-circle" size={16} color={colors.me.textSecondary} />
|
||||
<Text style={styles.tideHintText}>
|
||||
Last confirmation: 3 days ago · Next reminder in 4 days.
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View style={styles.tideModalButtons}>
|
||||
<TouchableOpacity
|
||||
style={styles.confirmPulseButton}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => setShowTideModal(false)}
|
||||
>
|
||||
<Ionicons name="checkmark-circle" size={18} color={colors.nautical.teal} />
|
||||
<Text style={styles.confirmPulseText}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.confirmPulseButton}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => setShowTideModal(false)}
|
||||
>
|
||||
<Ionicons name="close-circle" size={18} color={colors.nautical.teal} />
|
||||
<Text style={styles.confirmPulseText}>Close</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View style={styles.tideModalButtons}>
|
||||
<TouchableOpacity
|
||||
style={styles.confirmPulseButton}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => setShowTideModal(false)}
|
||||
>
|
||||
<Ionicons name="checkmark-circle" size={18} color={colors.nautical.teal} />
|
||||
<Text style={styles.confirmPulseText}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.confirmPulseButton}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => setShowTideModal(false)}
|
||||
>
|
||||
<Ionicons name="close-circle" size={18} color={colors.nautical.teal} />
|
||||
<Text style={styles.confirmPulseText}>Close</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
|
||||
290
src/screens/RegisterScreen.tsx
Normal file
290
src/screens/RegisterScreen.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, borderRadius, typography, shadows } from '../theme/colors';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { ActivityIndicator } from 'react-native';
|
||||
|
||||
export default function RegisterScreen({ navigation }: any) {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { signUp, isLoading } = useAuth();
|
||||
|
||||
const handleRegister = async () => {
|
||||
setError(null);
|
||||
if (!name || !email || !password || !confirmPassword) {
|
||||
setError('Please fill in all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
setError('Please enter a valid email address.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await signUp({ username: name, email, password });
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Registration failed. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.content}
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<Feather name="arrow-left" size={24} color={colors.sentinel.text} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.header}>
|
||||
<View style={styles.logoContainer}>
|
||||
<MaterialCommunityIcons name="compass-outline" size={48} color={colors.sentinel.primary} />
|
||||
</View>
|
||||
<Text style={styles.title}>Join the Fleet</Text>
|
||||
<Text style={styles.subtitle}>Begin your journey with Sentinel</Text>
|
||||
{error && (
|
||||
<View style={styles.errorContainer}>
|
||||
<MaterialCommunityIcons name="alert-circle-outline" size={20} color={colors.sentinel.statusCritical} />
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Feather name="user" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Captain's Name"
|
||||
placeholderTextColor={colors.sentinel.textSecondary}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Feather name="mail" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
placeholderTextColor={colors.sentinel.textSecondary}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Feather name="lock" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
placeholderTextColor={colors.sentinel.textSecondary}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Feather name="check-circle" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Confirm Password"
|
||||
placeholderTextColor={colors.sentinel.textSecondary}
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.registerButton, isLoading && styles.disabledButton]}
|
||||
activeOpacity={0.8}
|
||||
onPress={handleRegister}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={colors.white} />
|
||||
) : (
|
||||
<Text style={styles.registerButtonText}>Create Account</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.loginLink}
|
||||
onPress={() => navigation.navigate('Login')}
|
||||
>
|
||||
<Text style={styles.loginLinkText}>
|
||||
Already have an account? <Text style={styles.highlight}>Login</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
backButton: {
|
||||
marginTop: spacing.md,
|
||||
marginBottom: spacing.lg,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: borderRadius.full,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
logoContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: borderRadius.full,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(184, 224, 229, 0.2)',
|
||||
},
|
||||
title: {
|
||||
fontSize: typography.fontSize.xxl,
|
||||
fontWeight: 'bold',
|
||||
color: colors.sentinel.text,
|
||||
marginBottom: spacing.xs,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.sentinel.textSecondary,
|
||||
},
|
||||
form: {
|
||||
width: '100%',
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(184, 224, 229, 0.1)',
|
||||
marginBottom: spacing.base,
|
||||
height: 56,
|
||||
paddingHorizontal: spacing.base,
|
||||
},
|
||||
inputIcon: {
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
color: colors.sentinel.text,
|
||||
fontSize: typography.fontSize.base,
|
||||
},
|
||||
registerButton: {
|
||||
backgroundColor: colors.nautical.seafoam,
|
||||
height: 56,
|
||||
borderRadius: borderRadius.lg,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: spacing.md,
|
||||
...shadows.glow,
|
||||
},
|
||||
registerButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: typography.fontSize.md,
|
||||
fontWeight: 'bold',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
loginLink: {
|
||||
alignItems: 'center',
|
||||
marginTop: spacing.xl,
|
||||
},
|
||||
loginLinkText: {
|
||||
color: colors.sentinel.textSecondary,
|
||||
fontSize: typography.fontSize.base,
|
||||
},
|
||||
highlight: {
|
||||
color: colors.nautical.seafoam,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
disabledButton: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
errorContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 68, 68, 0.1)',
|
||||
padding: spacing.sm,
|
||||
borderRadius: borderRadius.md,
|
||||
marginTop: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 68, 68, 0.2)',
|
||||
},
|
||||
errorText: {
|
||||
color: colors.sentinel.statusCritical,
|
||||
fontSize: typography.fontSize.sm,
|
||||
marginLeft: spacing.xs,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
80
src/services/auth.service.ts
Normal file
80
src/services/auth.service.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { LoginRequest, LoginResponse, RegisterRequest, User } from '../types';
|
||||
|
||||
const PLATFORM_URL = 'http://192.168.56.103:8000';
|
||||
const no_backend_mode = true;
|
||||
|
||||
const MOCK_USER: User = {
|
||||
id: 999,
|
||||
username: 'MockCaptain',
|
||||
public_key: 'mock_public_key',
|
||||
is_admin: true,
|
||||
guale: false,
|
||||
tier: 'premium',
|
||||
tier_expires_at: '2026-12-31T23:59:59Z',
|
||||
last_active_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
export const authService = {
|
||||
async login(credentials: LoginRequest): Promise<LoginResponse> {
|
||||
if (no_backend_mode) {
|
||||
console.log('No-Backend Mode: Simulating login...');
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
access_token: 'mock_access_token',
|
||||
token_type: 'bearer',
|
||||
user: { ...MOCK_USER, username: credentials.username },
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${PLATFORM_URL}/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async register(data: RegisterRequest): Promise<User> {
|
||||
if (no_backend_mode) {
|
||||
console.log('No-Backend Mode: Simulating registration...');
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ ...MOCK_USER, username: data.username });
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${PLATFORM_URL}/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail?.[0]?.msg || 'Registration failed');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -13,7 +13,7 @@ export interface FlowRecord {
|
||||
}
|
||||
|
||||
// Vault Types
|
||||
export type VaultAssetType =
|
||||
export type VaultAssetType =
|
||||
| 'game_account'
|
||||
| 'private_key'
|
||||
| 'document'
|
||||
@@ -72,3 +72,32 @@ export interface ProtocolInfo {
|
||||
version: string;
|
||||
lastUpdated: Date;
|
||||
}
|
||||
|
||||
// Auth Types
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
public_key: string;
|
||||
is_admin: boolean;
|
||||
guale: boolean;
|
||||
tier: string;
|
||||
tier_expires_at: string;
|
||||
last_active_at: string;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user