/// import React, { createContext, useContext, useState, useEffect, ReactNode } from "react"; import * as SecureStore from "expo-secure-store"; import { Platform } from "react-native"; import { WebAuthService } from "../services/webAuth"; import { CalComAPIService } from "../services/calcom"; import { createCalComOAuthService, OAuthTokens, CalComOAuthService, } from "../services/oauthService"; interface AuthContextType { isAuthenticated: boolean; accessToken: string | null; refreshToken: string | null; userInfo: any; isWebSession: boolean; loginFromWebSession: (userInfo: any) => Promise; loginWithOAuth: () => Promise; logout: () => Promise; loading: boolean; } const AuthContext = createContext(undefined); const ACCESS_TOKEN_KEY = "cal_access_token"; const REFRESH_TOKEN_KEY = "cal_refresh_token"; const OAUTH_TOKENS_KEY = "cal_oauth_tokens"; const AUTH_TYPE_KEY = "cal_auth_type"; type AuthType = "oauth" | "web_session"; interface AuthProviderProps { children: ReactNode; } // Check if chrome.storage is available (browser extension context) const isChromeStorageAvailable = (): boolean => { return ( Platform.OS === "web" && typeof chrome !== "undefined" && chrome.storage !== undefined && chrome.storage.local !== undefined ); }; // Unified storage helper to abstract web/mobile/extension differences const storage = { get: async (key: string): Promise => { // Use chrome.storage in browser extension context (most secure) if (isChromeStorageAvailable()) { return new Promise((resolve) => { chrome.storage.local.get([key], (result) => { resolve(result[key] || null); }); }); } // Fall back to localStorage for regular web apps if (Platform.OS === "web") { return localStorage.getItem(key); } // Use SecureStore for mobile return await SecureStore.getItemAsync(key); }, set: async (key: string, value: string): Promise => { // Use chrome.storage in browser extension context (most secure) if (isChromeStorageAvailable()) { return new Promise((resolve, reject) => { chrome.storage.local.set({ [key]: value }, () => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { resolve(); } }); }); } // Fall back to localStorage for regular web apps if (Platform.OS === "web") { localStorage.setItem(key, value); return; } // Use SecureStore for mobile await SecureStore.setItemAsync(key, value); }, remove: async (key: string): Promise => { // Use chrome.storage in browser extension context (most secure) if (isChromeStorageAvailable()) { return new Promise((resolve, reject) => { chrome.storage.local.remove(key, () => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { resolve(); } }); }); } // Fall back to localStorage for regular web apps if (Platform.OS === "web") { localStorage.removeItem(key); return; } // Use SecureStore for mobile await SecureStore.deleteItemAsync(key); }, removeAll: async (keys: string[]): Promise => { // Use chrome.storage in browser extension context (most secure) if (isChromeStorageAvailable()) { return new Promise((resolve, reject) => { chrome.storage.local.remove(keys, () => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { resolve(); } }); }); } // Fall back to localStorage for regular web apps if (Platform.OS === "web") { keys.forEach((key) => localStorage.removeItem(key)); return; } // Use SecureStore for mobile await Promise.all(keys.map((key) => SecureStore.deleteItemAsync(key))); }, }; export function AuthProvider({ children }: AuthProviderProps) { const [isAuthenticated, setIsAuthenticated] = useState(false); const [accessToken, setAccessToken] = useState(null); const [refreshToken, setRefreshToken] = useState(null); const [userInfo, setUserInfo] = useState(null); const [isWebSession, setIsWebSession] = useState(false); const [loading, setLoading] = useState(true); const [oauthService] = useState(() => { try { return createCalComOAuthService(); } catch (error) { console.warn("Failed to initialize OAuth service:", error); return null; } }); // Setup refresh token function for OAuth const setupRefreshTokenFunction = (service: CalComOAuthService) => { CalComAPIService.setRefreshTokenFunction(async (refreshToken: string) => { const newTokens = await service.refreshAccessToken(refreshToken); return { accessToken: newTokens.accessToken, refreshToken: newTokens.refreshToken, }; }); }; // Common post-login setup: configure API service and fetch user profile const setupAfterLogin = async (token: string, refreshToken?: string) => { CalComAPIService.setAccessToken(token, refreshToken); try { await CalComAPIService.getUserProfile(); } catch (profileError) { console.error("Failed to fetch user profile:", profileError); // Don't fail login if profile fetch fails } }; // Save OAuth tokens to storage const saveOAuthTokens = async (tokens: OAuthTokens) => { await storage.set(OAUTH_TOKENS_KEY, JSON.stringify(tokens)); await storage.set(AUTH_TYPE_KEY, "oauth"); }; // Clear all auth data from storage const clearAuth = async () => { await storage.removeAll([ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, OAUTH_TOKENS_KEY, AUTH_TYPE_KEY]); }; // Reset all auth state const resetAuthState = () => { setAccessToken(null); setRefreshToken(null); setUserInfo(null); setIsAuthenticated(false); setIsWebSession(false); CalComAPIService.clearAuth(); CalComAPIService.clearUserProfile(); }; useEffect(() => { checkAuthState(); // Set up token refresh callback const handleTokenRefresh = async (newAccessToken: string, newRefreshToken?: string) => { try { const tokens: OAuthTokens = { accessToken: newAccessToken, refreshToken: newRefreshToken || refreshToken || undefined, tokenType: "Bearer", }; await saveOAuthTokens(tokens); setAccessToken(newAccessToken); if (newRefreshToken) { setRefreshToken(newRefreshToken); } CalComAPIService.setAccessToken( newAccessToken, newRefreshToken || refreshToken || undefined ); } catch (error) { console.error("Failed to handle token refresh:", error); await logout(); } }; CalComAPIService.setTokenRefreshCallback(handleTokenRefresh); return () => { CalComAPIService.setTokenRefreshCallback(() => Promise.resolve()); }; }, [refreshToken]); // Handle OAuth authentication const handleOAuthAuth = async (storedTokens: OAuthTokens) => { if (!oauthService) return; // Refresh token if expired let tokens = storedTokens; if (oauthService.isTokenExpired(storedTokens) && storedTokens.refreshToken) { try { console.log("Access token expired, refreshing..."); tokens = await oauthService.refreshAccessToken(storedTokens.refreshToken); await saveOAuthTokens(tokens); } catch (refreshError) { console.error("Failed to refresh token:", refreshError); await clearAuth(); return; } } // Set state setAccessToken(tokens.accessToken); setRefreshToken(tokens.refreshToken || null); setIsAuthenticated(true); setIsWebSession(false); // Setup API service and refresh function await setupAfterLogin(tokens.accessToken, tokens.refreshToken); if (tokens.refreshToken) { setupRefreshTokenFunction(oauthService); } }; // Handle web session authentication const handleWebSessionAuth = () => { setIsWebSession(true); }; const checkAuthState = async () => { try { const authType = (await storage.get(AUTH_TYPE_KEY)) as AuthType | null; const storedOAuthTokens = await storage.get(OAUTH_TOKENS_KEY); const storedTokens = storedOAuthTokens ? JSON.parse(storedOAuthTokens) : null; if (authType === "oauth" && storedTokens && oauthService) { await handleOAuthAuth(storedTokens); } else if (authType === "web_session") { handleWebSessionAuth(); } } catch (error) { console.error("Failed to check auth state:", error); } finally { setLoading(false); } }; const loginFromWebSession = async (sessionUserInfo: any) => { try { setUserInfo(sessionUserInfo); setIsAuthenticated(true); setIsWebSession(true); await storage.set(AUTH_TYPE_KEY, "web_session"); // Try to get tokens from web session const tokens = await WebAuthService.getTokensFromWebSession(); if (tokens.accessToken) { setAccessToken(tokens.accessToken); setRefreshToken(tokens.refreshToken || null); await setupAfterLogin(tokens.accessToken, tokens.refreshToken); } } catch (error) { console.error("Failed to login from web session:", error); throw error; } }; const loginWithOAuth = async (): Promise => { if (!oauthService) { throw new Error("OAuth service not available. Please check your configuration."); } try { setLoading(true); const tokens = await oauthService.startAuthorizationFlow(); // Save tokens await saveOAuthTokens(tokens); // Update state setAccessToken(tokens.accessToken); setRefreshToken(tokens.refreshToken || null); setIsAuthenticated(true); setIsWebSession(false); // Setup API service and refresh function await setupAfterLogin(tokens.accessToken, tokens.refreshToken); if (tokens.refreshToken) { setupRefreshTokenFunction(oauthService); } // Clear PKCE parameters oauthService.clearPKCEParams(); } catch (error) { console.error("OAuth login failed:", error); throw error; } finally { setLoading(false); } }; const logout = async () => { try { await clearAuth(); resetAuthState(); } catch (error) { console.error("Failed to logout:", error); } }; const value: AuthContextType = { isAuthenticated, accessToken, refreshToken, userInfo, isWebSession, loginFromWebSession, loginWithOAuth, logout, loading, }; return {children}; } export function useAuth(): AuthContextType { const context = useContext(AuthContext); if (context === undefined) { throw new Error("useAuth must be used within an AuthProvider"); } return context; }