diff --git a/app/login/login/SocialForm.tsx b/app/login/login/SocialForm.tsx index 3cecdba..18df140 100755 --- a/app/login/login/SocialForm.tsx +++ b/app/login/login/SocialForm.tsx @@ -1,7 +1,6 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faApple, faFacebookF, faGoogle} from "@fortawesome/free-brands-svg-icons"; import React, {useContext, useEffect} from "react"; -import Link from "next/link"; import System from "@/lib/models/System"; import {AlertContext} from "@/context/AlertContext"; import {configs} from "@/lib/configs"; @@ -14,8 +13,12 @@ export default function SocialForm() { const {setSession} = useContext(SessionContext) const t = useTranslations(); const {lang} = useContext(LangContext) - + const isElectron = typeof window !== 'undefined' && !!window.electron; + useEffect((): void => { + // Skip URL parsing in Electron (OAuth is handled via BrowserWindow) + if (isElectron) return; + const params = new URLSearchParams(window.location.search); const provider: string | null = params.get('provider'); if (!provider) { @@ -47,6 +50,16 @@ export default function SocialForm() { } }, []); + async function handleLoginSuccess(token: string): Promise { + if (window.electron) { + await window.electron.setToken(token); + window.electron.loginSuccess(token); + } else { + System.setCookie('token', token, 30); + setSession({isConnected: true, user: null, accessToken: token}); + } + } + async function handleFacebookLogin(code: string, state: string): Promise { if (code && state) { const response: string = await System.postToServer(`auth/facebook`, { @@ -57,16 +70,10 @@ export default function SocialForm() { errorMessage(t('socialForm.error.connection')); return; } - System.setCookie('token', response, 30); - const token: string | null = System.getCookie('token'); - if (!token) { - errorMessage(t('socialForm.error.connection')); - return; - } - setSession({isConnected: true, user: null, accessToken: token}); + await handleLoginSuccess(response); } } - + async function handleGoogleLogin(code: string): Promise { if (code) { const response: string = await System.postToServer(`auth/google`, { @@ -74,17 +81,12 @@ export default function SocialForm() { }, lang); if (!response) { errorMessage(t('socialForm.error.connection')); - } - System.setCookie('token', response, 30); - const token: string | null = System.getCookie('token'); - if (!token) { - errorMessage(t('socialForm.error.connection')); return; } - setSession({isConnected: true, user: null, accessToken: token}); + await handleLoginSuccess(response); } } - + async function handleAppleLogin(code: string, state: string): Promise { if (code && state) { const response: string = await System.postToServer(`auth/apple`, { @@ -95,41 +97,62 @@ export default function SocialForm() { errorMessage(t('socialForm.error.connection')); return; } - System.setCookie('token', response, 30); - const token: string | null = System.getCookie('token'); - if (!token) { - errorMessage(t('socialForm.error.connection')); - return; - } - setSession({isConnected: true, user: null, accessToken: token}); + await handleLoginSuccess(response); } } + async function handleOAuthClick(provider: 'google' | 'facebook' | 'apple'): Promise { + if (!window.electron) return; + + try { + const result = await window.electron.oauthLogin(provider, configs.baseUrl); + + if (!result.success) { + if (result.error !== 'Window closed by user') { + errorMessage(t('socialForm.error.connection')); + } + return; + } + + if (result.code) { + if (provider === 'google') { + await handleGoogleLogin(result.code); + } else if (provider === 'facebook' && result.state) { + await handleFacebookLogin(result.code, result.state); + } else if (provider === 'apple' && result.state) { + await handleAppleLogin(result.code, result.state); + } + } + } catch (error) { + errorMessage(t('socialForm.error.connection')); + } + } + return (
- handleOAuthClick('facebook')} + className="flex items-center justify-center w-14 h-14 bg-[#1877F2] hover:bg-opacity-90 text-textPrimary rounded-xl transition-colors cursor-pointer" aria-label="Login with Facebook" > - + - handleOAuthClick('google')} + className="flex items-center justify-center w-14 h-14 bg-white hover:bg-opacity-90 text-[#4285F4] rounded-xl transition-colors cursor-pointer" aria-label="Login with Google" > - - - + +
) } diff --git a/app/page.tsx b/app/page.tsx index 3c20b40..b92c12d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -456,6 +456,7 @@ function ScribeContent() { user: user, accessToken: token, }); + console.log(user) setCurrentCredits(user.creditsBalance) setAmountSpent(user.aiUsage) } catch (e: unknown) { @@ -601,14 +602,8 @@ function ScribeContent() { - -
+ +
diff --git a/components/CreditMeters.tsx b/components/CreditMeters.tsx index cf82719..da83f0b 100644 --- a/components/CreditMeters.tsx +++ b/components/CreditMeters.tsx @@ -1,11 +1,11 @@ -import {useContext} from "react"; +import React, {useContext} from "react"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faCoins, faDollarSign} from "@fortawesome/free-solid-svg-icons"; import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext"; export default function CreditCounter({isCredit}: { isCredit: boolean }) { const {totalCredits, totalPrice} = useContext(AIUsageContext) - + if (isCredit) { return (
); } - + return (
diff --git a/electron.d.ts b/electron.d.ts index 1c1bdc2..2bbbd1d 100644 --- a/electron.d.ts +++ b/electron.d.ts @@ -40,6 +40,14 @@ export interface IElectronAPI { // Open external links (browser/native app) openExternal: (url: string) => Promise; + // OAuth login via BrowserWindow + oauthLogin: (provider: 'google' | 'facebook' | 'apple', baseUrl: string) => Promise<{ + success: boolean; + code?: string; + state?: string; + error?: string; + }>; + // Offline mode management offlinePinSet: (pin: string) => Promise<{ success: boolean; error?: string }>; offlinePinVerify: (pin: string) => Promise<{ success: boolean; userId?: string; error?: string }>; diff --git a/electron/main.ts b/electron/main.ts index f473d67..391ca0d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -188,6 +188,114 @@ ipcMain.handle('open-external', async (_event, url: string) => { } }); +// IPC Handler pour OAuth login via BrowserWindow +let oauthWindow: BrowserWindow | null = null; + +interface OAuthResult { + success: boolean; + code?: string; + state?: string; + error?: string; +} + +interface OAuthRequest { + provider: 'google' | 'facebook' | 'apple'; + baseUrl: string; +} + +ipcMain.handle('oauth-login', async (_event, request: OAuthRequest): Promise => { + return new Promise((resolve) => { + const { provider, baseUrl } = request; + const redirectUri = `${baseUrl}login?provider=${provider}`; + const encodedRedirectUri = encodeURIComponent(redirectUri); + + // Fermer une éventuelle fenêtre OAuth existante + if (oauthWindow) { + oauthWindow.close(); + oauthWindow = null; + } + + // Configuration OAuth par provider + const oauthConfigs: Record = { + google: `https://accounts.google.com/o/oauth2/v2/auth?client_id=911482317931-pvjog1br22r6l8k1afq0ki94em2fsoen.apps.googleusercontent.com&redirect_uri=${encodedRedirectUri}&response_type=code&scope=openid%20email%20profile&access_type=offline`, + facebook: `https://www.facebook.com/v18.0/dialog/oauth?client_id=1015270470233591&redirect_uri=${encodedRedirectUri}&scope=email&response_type=code&state=abc123`, + apple: `https://appleid.apple.com/auth/authorize?client_id=eritors.apple.login&redirect_uri=${encodedRedirectUri}&response_type=code&scope=email%20name&response_mode=query&state=abc123` + }; + + const authUrl = oauthConfigs[provider]; + if (!authUrl) { + resolve({ success: false, error: 'Invalid provider' }); + return; + } + + oauthWindow = new BrowserWindow({ + width: 600, + height: 700, + show: true, + autoHideMenuBar: true, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + } + }); + + // Intercepter les redirections pour capturer le code OAuth + const handleNavigation = (url: string): boolean => { + try { + const parsedUrl = new URL(url); + + // Vérifier si c'est notre redirect URI (compare sans le query string) + const baseRedirectUri = `${baseUrl}login`; + if (url.startsWith(baseRedirectUri)) { + const code = parsedUrl.searchParams.get('code'); + const state = parsedUrl.searchParams.get('state'); + const error = parsedUrl.searchParams.get('error'); + + if (error) { + resolve({ success: false, error }); + } else if (code) { + resolve({ success: true, code, state: state || undefined }); + } else { + resolve({ success: false, error: 'No code received' }); + } + + if (oauthWindow) { + oauthWindow.close(); + oauthWindow = null; + } + return true; // Navigation interceptée + } + } catch (e) { + // URL invalide, continuer + } + return false; // Laisser la navigation continuer + }; + + // Écouter will-redirect (redirections HTTP) + oauthWindow.webContents.on('will-redirect', (event, url) => { + if (handleNavigation(url)) { + event.preventDefault(); + } + }); + + // Écouter will-navigate (navigations normales) + oauthWindow.webContents.on('will-navigate', (event, url) => { + if (handleNavigation(url)) { + event.preventDefault(); + } + }); + + // Gérer la fermeture de la fenêtre par l'utilisateur + oauthWindow.on('closed', () => { + oauthWindow = null; + resolve({ success: false, error: 'Window closed by user' }); + }); + + // Charger l'URL OAuth + oauthWindow.loadURL(authUrl); + }); +}); + // IPC Handlers pour la gestion du token (OS-encrypted storage) ipcMain.handle('get-token', () => { const storage:SecureStorage = getSecureStorage(); diff --git a/electron/preload.ts b/electron/preload.ts index f16051c..0285a41 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -38,6 +38,10 @@ contextBridge.exposeInMainWorld('electron', { // Open external links (browser/native app) openExternal: (url: string) => ipcRenderer.invoke('open-external', url), + // OAuth login via BrowserWindow + oauthLogin: (provider: 'google' | 'facebook' | 'apple', baseUrl: string) => + ipcRenderer.invoke('oauth-login', { provider, baseUrl }), + // Offline mode management offlinePinSet: (pin: string) => ipcRenderer.invoke('offline:pin:set', { pin }), offlinePinVerify: (pin: string) => ipcRenderer.invoke('offline:pin:verify', { pin }), diff --git a/lib/models/QuillSense.ts b/lib/models/QuillSense.ts index a777eb0..5078d5b 100644 --- a/lib/models/QuillSense.ts +++ b/lib/models/QuillSense.ts @@ -19,7 +19,8 @@ export interface Conversation { type?: ConversationType; messages: Message[]; status: number; - totalPrice?: number; + totalPrice?: number + useYourKey?: boolean; } export interface AIGeneratedText { @@ -34,21 +35,25 @@ export interface AIResponseWithCredits { data: T; } -export interface AIDictionary extends AIResponseWithCredits {} +export interface AIDictionary extends AIResponseWithCredits { +} export interface AIGeneratedTextData { totalCost: number; response: string; } -export interface AIGeneratedText extends AIResponseWithCredits {} -export interface AIInspire extends AIResponseWithCredits {} +export interface AIGeneratedText extends AIResponseWithCredits { +} -export interface AISynonyms extends AIResponseWithCredits {} +export interface AIInspire extends AIResponseWithCredits { +} -export interface AIVerbConjugation extends AIResponseWithCredits {} +export interface AISynonyms extends AIResponseWithCredits { +} -export interface AISimpleText extends AIResponseWithCredits {} +export interface AIVerbConjugation extends AIResponseWithCredits { +} interface InspireAIResponse { ideas: { @@ -80,10 +85,6 @@ export interface InspirationAIIdea { relatedTo: string; } -export interface InspiredAIResponse { - ideas: InspirationAIIdea[]; -} - export interface ConversationProps { id: string; mode: string; @@ -94,8 +95,13 @@ export interface ConversationProps { export default class QuillSense { static getSubLevel(session: SessionProps): number { - const currentSub: Subscription | null = User.getCurrentSubscription(session?.user, 'quill-sense'); - if (!currentSub) return 0; + let currentSub: Subscription | null = User.getCurrentSubscription(session?.user, 'quill-sense'); + if (!currentSub) { + currentSub = User.getCurrentSubscription(session?.user, 'quill-trial'); + if (!currentSub) { + return 0; + } + } switch (currentSub?.subTier) { case 1: return 1; diff --git a/lib/models/User.ts b/lib/models/User.ts index 58e9996..98e7528 100644 --- a/lib/models/User.ts +++ b/lib/models/User.ts @@ -49,7 +49,7 @@ export const writingLevel: SelectBoxProps[] = [ ]; export default class User { - static getCurrentSubscription(user: UserProps | null, type: "quill-sense" | "use-your-keys"): Subscription | null { + static getCurrentSubscription(user: UserProps | null, type: "quill-sense" | "use-your-keys" | "quill-trial"): Subscription | null { if (!user || !user.subscription || user.subscription.length === 0) { return null; }