mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-25 14:17:57 +00:00
298a2c57f1
- Add mobile-bridge.ts utility for native app communication - Add useMobileBridge.ts React hook - Update AuthContext to detect and use native wallet - Update PezkuwiContext to connect mobile wallet automatically - Update WalletContext to sign transactions via native bridge Mobile app can now seamlessly use web P2P features with native wallet.
309 lines
9.5 KiB
TypeScript
309 lines
9.5 KiB
TypeScript
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
import { supabase } from '@/lib/supabase';
|
|
import { User } from '@supabase/supabase-js';
|
|
import { isMobileApp, getNativeWalletAddress, getNativeAccountName } from '@/lib/mobile-bridge';
|
|
|
|
// 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 = 'last_activity_timestamp';
|
|
const REMEMBER_ME_KEY = 'remember_me';
|
|
|
|
interface AuthContextType {
|
|
user: User | null;
|
|
loading: boolean;
|
|
isAdmin: boolean;
|
|
signIn: (email: string, password: string, rememberMe?: boolean) => Promise<{ error: Error | null }>;
|
|
signUp: (email: string, password: string, username: string, referralCode?: string) => Promise<{ error: Error | null }>;
|
|
signOut: () => Promise<void>;
|
|
checkAdminStatus: () => Promise<boolean>;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(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<User | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isAdmin, setIsAdmin] = useState(false);
|
|
|
|
// ========================================
|
|
// SESSION TIMEOUT MANAGEMENT
|
|
// ========================================
|
|
|
|
// Update last activity timestamp
|
|
const updateLastActivity = useCallback(() => {
|
|
|
|
localStorage.setItem(LAST_ACTIVITY_KEY, Date.now().toString());
|
|
}, []);
|
|
|
|
const signOut = useCallback(async () => {
|
|
setIsAdmin(false);
|
|
setUser(null);
|
|
localStorage.removeItem(LAST_ACTIVITY_KEY);
|
|
localStorage.removeItem(REMEMBER_ME_KEY);
|
|
await supabase.auth.signOut();
|
|
}, []);
|
|
|
|
// Check if session has timed out
|
|
const checkSessionTimeout = useCallback(async () => {
|
|
if (!user) return;
|
|
|
|
// Skip timeout check if "Remember Me" is enabled
|
|
const rememberMe = localStorage.getItem(REMEMBER_ME_KEY);
|
|
if (rememberMe === 'true') {
|
|
return; // Don't timeout if user chose to be remembered
|
|
}
|
|
|
|
const lastActivity = localStorage.getItem(LAST_ACTIVITY_KEY);
|
|
if (!lastActivity) {
|
|
updateLastActivity();
|
|
return;
|
|
}
|
|
|
|
const lastActivityTime = parseInt(lastActivity, 10);
|
|
const now = Date.now();
|
|
const inactiveTime = now - lastActivityTime;
|
|
|
|
if (inactiveTime >= SESSION_TIMEOUT_MS) {
|
|
if (import.meta.env.DEV) console.log('⏱️ Session timeout - logging out due to inactivity');
|
|
await signOut();
|
|
}
|
|
}, [user, updateLastActivity, signOut]);
|
|
|
|
// Setup activity listeners
|
|
useEffect(() => {
|
|
if (!user) return;
|
|
|
|
// Update activity on user interactions
|
|
const activityEvents = ['mousedown', 'keydown', 'scroll', 'touchstart'];
|
|
|
|
const handleActivity = () => {
|
|
updateLastActivity();
|
|
|
|
|
|
};
|
|
|
|
// Register event listeners
|
|
activityEvents.forEach((event) => {
|
|
window.addEventListener(event, handleActivity);
|
|
});
|
|
|
|
// Initial activity timestamp
|
|
updateLastActivity();
|
|
|
|
|
|
|
|
// Check for timeout periodically
|
|
const timeoutChecker = setInterval(checkSessionTimeout, ACTIVITY_CHECK_INTERVAL_MS);
|
|
|
|
// Cleanup
|
|
return () => {
|
|
activityEvents.forEach((event) => {
|
|
window.removeEventListener(event, handleActivity);
|
|
});
|
|
clearInterval(timeoutChecker);
|
|
};
|
|
}, [user, updateLastActivity, checkSessionTimeout]);
|
|
|
|
|
|
const checkAdminStatus = useCallback(async () => {
|
|
// Admin wallet whitelist (blockchain-based auth)
|
|
const ADMIN_WALLETS = [
|
|
'5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', // Founder (original)
|
|
'5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3', // Founder delegate (initial KYC member)
|
|
'5GgTgG9sRmPQAYU1RsTejZYnZRjwzKZKWD3awtuqjHioki45', // Founder (current dev wallet)
|
|
];
|
|
|
|
try {
|
|
// PRIMARY: Check wallet-based admin (blockchain auth)
|
|
const connectedWallet = localStorage.getItem('selectedWallet');
|
|
if (import.meta.env.DEV) console.log('🔍 Admin check - Connected wallet:', connectedWallet);
|
|
if (import.meta.env.DEV) console.log('🔍 Admin check - Whitelist:', ADMIN_WALLETS);
|
|
|
|
if (connectedWallet && ADMIN_WALLETS.includes(connectedWallet)) {
|
|
if (import.meta.env.DEV) console.log('✅ Admin access granted (wallet-based)');
|
|
setIsAdmin(true);
|
|
return true;
|
|
}
|
|
|
|
// SECONDARY: Check Supabase admin_roles (if wallet not in whitelist)
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
if (user) {
|
|
const { data, error } = await supabase
|
|
.from('admin_roles')
|
|
.select('role')
|
|
.eq('user_id', user.id)
|
|
.maybeSingle();
|
|
|
|
if (!error && data && ['admin', 'super_admin'].includes(data.role)) {
|
|
if (import.meta.env.DEV) console.log('✅ Admin access granted (Supabase-based)');
|
|
setIsAdmin(true);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (import.meta.env.DEV) console.log('❌ Admin access denied');
|
|
setIsAdmin(false);
|
|
return false;
|
|
} catch (err) {
|
|
if (import.meta.env.DEV) console.error('Admin check error:', err);
|
|
setIsAdmin(false);
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
// Setup native mobile wallet if running in mobile app
|
|
const setupMobileWallet = useCallback(() => {
|
|
if (isMobileApp()) {
|
|
const nativeAddress = getNativeWalletAddress();
|
|
const nativeAccountName = getNativeAccountName();
|
|
|
|
if (nativeAddress) {
|
|
// Store native wallet address for admin checks and wallet operations
|
|
localStorage.setItem('selectedWallet', nativeAddress);
|
|
if (nativeAccountName) {
|
|
localStorage.setItem('selectedWalletName', nativeAccountName);
|
|
}
|
|
if (import.meta.env.DEV) {
|
|
console.log('[Mobile] Native wallet detected:', nativeAddress);
|
|
}
|
|
// Dispatch wallet change event
|
|
window.dispatchEvent(new Event('walletChanged'));
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
// Setup mobile wallet first
|
|
setupMobileWallet();
|
|
|
|
// Check active sessions and sets the user
|
|
supabase.auth.getSession().then(({ data: { session } }) => {
|
|
setUser(session?.user ?? null);
|
|
checkAdminStatus(); // Check admin status regardless of Supabase session
|
|
setLoading(false);
|
|
}).catch(() => {
|
|
// If Supabase is not available, still check wallet-based admin
|
|
checkAdminStatus();
|
|
setLoading(false);
|
|
});
|
|
|
|
// Listen for changes on auth state
|
|
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
|
setUser(session?.user ?? null);
|
|
checkAdminStatus(); // Check admin status on auth change
|
|
setLoading(false);
|
|
});
|
|
|
|
// Listen for wallet changes (from PezkuwiContext or native bridge)
|
|
const handleWalletChange = () => {
|
|
checkAdminStatus();
|
|
};
|
|
window.addEventListener('walletChanged', handleWalletChange);
|
|
|
|
// Listen for native bridge ready event (mobile app)
|
|
const handleNativeReady = () => {
|
|
if (import.meta.env.DEV) {
|
|
console.log('[Mobile] Native bridge ready');
|
|
}
|
|
setupMobileWallet();
|
|
checkAdminStatus();
|
|
};
|
|
window.addEventListener('pezkuwi-native-ready', handleNativeReady);
|
|
|
|
return () => {
|
|
subscription.unsubscribe();
|
|
window.removeEventListener('walletChanged', handleWalletChange);
|
|
window.removeEventListener('pezkuwi-native-ready', handleNativeReady);
|
|
};
|
|
}, [checkAdminStatus, setupMobileWallet]);
|
|
|
|
const signIn = async (email: string, password: string, rememberMe: boolean = false) => {
|
|
try {
|
|
const { data, error } = await supabase.auth.signInWithPassword({
|
|
email,
|
|
password,
|
|
});
|
|
|
|
if (!error && data.user) {
|
|
// Store remember me preference
|
|
if (rememberMe) {
|
|
localStorage.setItem(REMEMBER_ME_KEY, 'true');
|
|
} else {
|
|
localStorage.removeItem(REMEMBER_ME_KEY);
|
|
}
|
|
await checkAdminStatus();
|
|
}
|
|
|
|
return { error };
|
|
} catch {
|
|
return {
|
|
error: {
|
|
message: 'Authentication service unavailable. Please try again later.'
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
const signUp = async (email: string, password: string, username: string, referralCode?: string) => {
|
|
try {
|
|
const { data, error } = await supabase.auth.signUp({
|
|
email,
|
|
password,
|
|
options: {
|
|
data: {
|
|
username,
|
|
referral_code: referralCode || null,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!error && data.user) {
|
|
// Create profile in profiles table with referral code
|
|
await supabase.from('profiles').insert({
|
|
id: data.user.id,
|
|
username,
|
|
email,
|
|
referred_by: referralCode || null,
|
|
});
|
|
|
|
// If there's a referral code, track it
|
|
if (referralCode) {
|
|
// You can add logic here to reward the referrer
|
|
// For example, update their referral count or add rewards
|
|
if (import.meta.env.DEV) console.log(`User registered with referral code: ${referralCode}`);
|
|
}
|
|
}
|
|
|
|
return { error };
|
|
} catch {
|
|
return {
|
|
error: {
|
|
message: 'Registration service unavailable. Please try again later.'
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={{
|
|
user,
|
|
loading,
|
|
isAdmin,
|
|
signIn,
|
|
signUp,
|
|
signOut,
|
|
checkAdminStatus
|
|
}}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}; |