diff --git a/mobile/App.tsx b/mobile/App.tsx
index 06ae87ee..1c5dce0a 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -4,6 +4,7 @@ import { StatusBar } from 'expo-status-bar';
import { initializeI18n } from './src/i18n';
import { ErrorBoundary } from './src/components/ErrorBoundary';
import { LanguageProvider } from './src/contexts/LanguageContext';
+import { AuthProvider } from './src/contexts/AuthContext';
import { PolkadotProvider } from './src/contexts/PolkadotContext';
import { BiometricAuthProvider } from './src/contexts/BiometricAuthContext';
import AppNavigator from './src/navigation/AppNavigator';
@@ -38,14 +39,16 @@ export default function App() {
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/mobile/src/contexts/AuthContext.tsx b/mobile/src/contexts/AuthContext.tsx
new file mode 100644
index 00000000..0a1a47a9
--- /dev/null
+++ b/mobile/src/contexts/AuthContext.tsx
@@ -0,0 +1,243 @@
+import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
+import { supabase } from '../lib/supabase';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import type { User } from '@supabase/supabase-js';
+
+// Session timeout configuration
+const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
+const ACTIVITY_CHECK_INTERVAL_MS = 60 * 1000; // Check every 1 minute
+const LAST_ACTIVITY_KEY = '@pezkuwi_last_activity';
+
+interface AuthContextType {
+ user: User | null;
+ loading: boolean;
+ isAdmin: boolean;
+ signIn: (email: string, password: string) => Promise<{ error: Error | null }>;
+ signUp: (email: string, password: string, username: string, referralCode?: string) => Promise<{ error: Error | null }>;
+ signOut: () => Promise;
+ checkAdminStatus: () => Promise;
+ updateActivity: () => void;
+}
+
+const AuthContext = createContext(undefined);
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
+
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [isAdmin, setIsAdmin] = useState(false);
+
+ // Update last activity timestamp
+ const updateActivity = useCallback(async () => {
+ try {
+ await AsyncStorage.setItem(LAST_ACTIVITY_KEY, Date.now().toString());
+ } catch (error) {
+ if (__DEV__) console.error('Failed to update activity:', error);
+ }
+ }, []);
+
+ const signOut = useCallback(async () => {
+ setIsAdmin(false);
+ setUser(null);
+ await AsyncStorage.removeItem(LAST_ACTIVITY_KEY);
+ await supabase.auth.signOut();
+ }, []);
+
+ // Check if session has timed out
+ const checkSessionTimeout = useCallback(async () => {
+ if (!user) return;
+
+ try {
+ const lastActivity = await AsyncStorage.getItem(LAST_ACTIVITY_KEY);
+ if (!lastActivity) {
+ await updateActivity();
+ return;
+ }
+
+ const lastActivityTime = parseInt(lastActivity, 10);
+ const now = Date.now();
+ const inactiveTime = now - lastActivityTime;
+
+ if (inactiveTime >= SESSION_TIMEOUT_MS) {
+ if (__DEV__) console.log('⏱️ Session timeout - logging out due to inactivity');
+ await signOut();
+ }
+ } catch (error) {
+ if (__DEV__) console.error('Error checking session timeout:', error);
+ }
+ }, [user, updateActivity, signOut]);
+
+ // Setup activity monitoring
+ useEffect(() => {
+ if (!user) return;
+
+ // Initial activity timestamp
+ updateActivity();
+
+ // Check for timeout periodically
+ const timeoutChecker = setInterval(checkSessionTimeout, ACTIVITY_CHECK_INTERVAL_MS);
+
+ return () => {
+ clearInterval(timeoutChecker);
+ };
+ }, [user, updateActivity, checkSessionTimeout]);
+
+ // Check admin status
+ const checkAdminStatus = useCallback(async (): Promise => {
+ if (!user) return false;
+
+ try {
+ const { data, error } = await supabase
+ .from('profiles')
+ .select('is_admin')
+ .eq('id', user.id)
+ .single();
+
+ if (error) {
+ if (__DEV__) console.error('Error checking admin status:', error);
+ return false;
+ }
+
+ const adminStatus = data?.is_admin || false;
+ setIsAdmin(adminStatus);
+ return adminStatus;
+ } catch (error) {
+ if (__DEV__) console.error('Error in checkAdminStatus:', error);
+ return false;
+ }
+ }, [user]);
+
+ // Sign in function
+ const signIn = async (email: string, password: string): Promise<{ error: Error | null }> => {
+ try {
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
+
+ if (error) {
+ return { error };
+ }
+
+ if (data.user) {
+ setUser(data.user);
+ await updateActivity();
+ await checkAdminStatus();
+ }
+
+ return { error: null };
+ } catch (error) {
+ return { error: error as Error };
+ }
+ };
+
+ // Sign up function
+ const signUp = async (
+ email: string,
+ password: string,
+ username: string,
+ referralCode?: string
+ ): Promise<{ error: Error | null }> => {
+ try {
+ // Create auth user
+ const { data: authData, error: authError } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ data: {
+ username,
+ referral_code: referralCode,
+ },
+ },
+ });
+
+ if (authError) {
+ return { error: authError };
+ }
+
+ // Create profile
+ if (authData.user) {
+ const { error: profileError } = await supabase
+ .from('profiles')
+ .insert({
+ id: authData.user.id,
+ username,
+ email,
+ referral_code: referralCode,
+ });
+
+ if (profileError) {
+ if (__DEV__) console.error('Profile creation error:', profileError);
+ // Don't fail signup if profile creation fails
+ }
+
+ setUser(authData.user);
+ await updateActivity();
+ }
+
+ return { error: null };
+ } catch (error) {
+ return { error: error as Error };
+ }
+ };
+
+ // Initialize auth state
+ useEffect(() => {
+ const initAuth = async () => {
+ try {
+ // Get initial session
+ const { data: { session } } = await supabase.auth.getSession();
+
+ if (session?.user) {
+ setUser(session.user);
+ await checkAdminStatus();
+ await updateActivity();
+ }
+ } catch (error) {
+ if (__DEV__) console.error('Error initializing auth:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ initAuth();
+
+ // Listen for auth changes
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(
+ async (_event, session) => {
+ setUser(session?.user ?? null);
+
+ if (session?.user) {
+ await checkAdminStatus();
+ await updateActivity();
+ } else {
+ setIsAdmin(false);
+ }
+ }
+ );
+
+ return () => {
+ subscription.unsubscribe();
+ };
+ }, [checkAdminStatus, updateActivity]);
+
+ const value = {
+ user,
+ loading,
+ isAdmin,
+ signIn,
+ signUp,
+ signOut,
+ checkAdminStatus,
+ updateActivity,
+ };
+
+ return {children};
+};
diff --git a/mobile/src/lib/supabase.ts b/mobile/src/lib/supabase.ts
new file mode 100644
index 00000000..d6dd4433
--- /dev/null
+++ b/mobile/src/lib/supabase.ts
@@ -0,0 +1,23 @@
+import 'react-native-url-polyfill/auto';
+import { createClient } from '@supabase/supabase-js';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+// Initialize Supabase client from environment variables
+const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL || '';
+const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || '';
+
+if (!supabaseUrl || !supabaseKey) {
+ if (__DEV__) {
+ console.warn('Supabase credentials not found in environment variables');
+ console.warn('Add EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to .env');
+ }
+}
+
+export const supabase = createClient(supabaseUrl, supabaseKey, {
+ auth: {
+ storage: AsyncStorage,
+ autoRefreshToken: true,
+ persistSession: true,
+ detectSessionInUrl: false,
+ },
+});
diff --git a/mobile/src/screens/SignInScreen.tsx b/mobile/src/screens/SignInScreen.tsx
index 421482a8..9c760c37 100644
--- a/mobile/src/screens/SignInScreen.tsx
+++ b/mobile/src/screens/SignInScreen.tsx
@@ -10,9 +10,12 @@ import {
Platform,
ScrollView,
StatusBar,
+ Alert,
+ ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
+import { useAuth } from '../contexts/AuthContext';
import AppColors, { KurdistanColors } from '../theme/colors';
interface SignInScreenProps {
@@ -22,13 +25,35 @@ interface SignInScreenProps {
const SignInScreen: React.FC = ({ onSignIn, onNavigateToSignUp }) => {
const { t } = useTranslation();
+ const { signIn } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
- const handleSignIn = () => {
- // TODO: Implement actual authentication
- if (__DEV__) console.log('Sign in:', { email, password });
- onSignIn();
+ const handleSignIn = async () => {
+ if (!email || !password) {
+ Alert.alert('Error', 'Please enter both email and password');
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ const { error } = await signIn(email, password);
+
+ if (error) {
+ Alert.alert('Sign In Failed', error.message);
+ return;
+ }
+
+ // Success - navigate to app
+ onSignIn();
+ } catch (error) {
+ Alert.alert('Error', 'An unexpected error occurred');
+ if (__DEV__) console.error('Sign in error:', error);
+ } finally {
+ setIsLoading(false);
+ }
};
return (
@@ -91,11 +116,16 @@ const SignInScreen: React.FC = ({ onSignIn, onNavigateToSignU
- {t('auth.signIn')}
+ {isLoading ? (
+
+ ) : (
+ {t('auth.signIn')}
+ )}
@@ -223,6 +253,9 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
color: KurdistanColors.spi,
},
+ buttonDisabled: {
+ opacity: 0.6,
+ },
divider: {
flexDirection: 'row',
alignItems: 'center',
diff --git a/mobile/src/screens/SignUpScreen.tsx b/mobile/src/screens/SignUpScreen.tsx
index a94fe3bb..0ce0d582 100644
--- a/mobile/src/screens/SignUpScreen.tsx
+++ b/mobile/src/screens/SignUpScreen.tsx
@@ -10,9 +10,12 @@ import {
Platform,
ScrollView,
StatusBar,
+ Alert,
+ ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
+import { useAuth } from '../contexts/AuthContext';
import AppColors, { KurdistanColors } from '../theme/colors';
interface SignUpScreenProps {
@@ -22,18 +25,42 @@ interface SignUpScreenProps {
const SignUpScreen: React.FC = ({ onSignUp, onNavigateToSignIn }) => {
const { t } = useTranslation();
+ const { signUp } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
+ const [username, setUsername] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
- const handleSignUp = () => {
- // TODO: Implement actual registration
- if (password !== confirmPassword) {
- alert('Passwords do not match!');
+ const handleSignUp = async () => {
+ if (!email || !password || !username) {
+ Alert.alert('Error', 'Please fill all required fields');
return;
}
- if (__DEV__) console.log('Sign up:', { email, password });
- onSignUp();
+
+ if (password !== confirmPassword) {
+ Alert.alert('Error', 'Passwords do not match');
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ const { error } = await signUp(email, password, username);
+
+ if (error) {
+ Alert.alert('Sign Up Failed', error.message);
+ return;
+ }
+
+ // Success - navigate to app
+ onSignUp();
+ } catch (error) {
+ Alert.alert('Error', 'An unexpected error occurred');
+ if (__DEV__) console.error('Sign up error:', error);
+ } finally {
+ setIsLoading(false);
+ }
};
return (
@@ -77,6 +104,18 @@ const SignUpScreen: React.FC = ({ onSignUp, onNavigateToSignI
/>
+
+ {t('auth.username')}
+
+
+
{t('auth.password')}
= ({ onSignUp, onNavigateToSignI
- {t('auth.signUp')}
+ {isLoading ? (
+
+ ) : (
+ {t('auth.signUp')}
+ )}
@@ -226,6 +270,9 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
color: KurdistanColors.spi,
},
+ buttonDisabled: {
+ opacity: 0.6,
+ },
divider: {
flexDirection: 'row',
alignItems: 'center',