All files / core/blong-browser/src/components/OAuthCallback OAuthCallback.tsx

0% Statements 0/30
0% Branches 0/23
0% Functions 0/5
0% Lines 0/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107                                                                                                                                                                                                                     
/**
 * OAuthCallback — full-screen "Signing you in with Google…" screen.
 *
 * Reads `code`/`state` from the URL query string (the Google redirect target),
 * calls `onExchange` to swap the code for a token, then navigates to `/` on
 * success.  On failure it shows the error and a "Back to Login" button.
 */
import './OAuthCallback.css';
 
import {ProgressSpinner} from '../../primereact/index.js';
 
import React, {useEffect, useState} from 'react';
import {Button} from '../Button/Button.js';
 
export interface IOAuthCallbackProps {
    /** Swaps the authorization code for a session token (auth orchestrator). */
    onExchange: (params: {code: string; state?: string; redirectUri?: string}) => Promise<void>;
    /** Invoked after a successful exchange so the App can drop this screen. */
    onSuccess?: () => void;
    title?: string;
    logoIcon?: string;
}
 
export function OAuthCallback({onExchange, onSuccess, title, logoIcon = 'pi pi-google'}: IOAuthCallbackProps) {
    const [error, setError] = useState('');
    const [loading, setLoading] = useState(true);
 
    useEffect(() => {
        let cancelled = false;
        const run = async () => {
            const search = new URLSearchParams(window.location.search);
            const code = search.get('code') ?? '';
            const state = search.get('state') ?? undefined;
            // The redirect_uri must match the current URL (which may live under
            // a base path such as `/s/`).
            const redirectUri = `${window.location.origin}${window.location.pathname}`;
            if (!code) {
                if (!cancelled) {
                    setError('No authorization code was returned');
                    setLoading(false);
                }
                return;
            }
            try {
                await onExchange({code, state, redirectUri});
                if (!cancelled) {
                    // The exchange already stored the session token in the app
                    // store. Strip the callback suffix from the URL (the app
                    // may live under a base path such as `/s/`), then let the
                    // App re-render and show the portal.
                    const suffix = '/oauth/callback';
                    const base = window.location.pathname.endsWith(suffix)
                        ? window.location.pathname.slice(0, -suffix.length) || '/'
                        : window.location.pathname;
                    window.history.replaceState({}, '', base);
                    onSuccess?.();
                }
            } catch (err) {
                if (!cancelled) {
                    setError(String((err as Error).message ?? err));
                    setLoading(false);
                }
            }
        };
        void run();
        return () => {
            cancelled = true;
        };
    }, [onExchange, onSuccess]);
 
    const backToLogin = () => {
        window.location.href = '/';
    };
 
    return (
        <div className="blong-oauth-callback p-component">
            <div className="blong-oauth-callback__card">
                <i className={`blong-oauth-callback__icon ${logoIcon}`} />
                {loading ? (
                    <>
                        <p className="blong-oauth-callback__heading">
                            Signing you in with Google…
                        </p>
                        <ProgressSpinner
                            className="blong-oauth-callback__spinner"
                            style={{width: '3rem', height: '3rem'}}
                        />
                    </>
                ) : (
                    <>
                        <p className="blong-oauth-callback__heading">Sign in failed</p>
                        <p className="blong-oauth-callback__error">{error}</p>
                        <Button
                            label="Back to Login"
                            icon="pi pi-arrow-left"
                            className="p-button-outlined blong-oauth-callback__back"
                            onClick={backToLogin}
                            data-testid="oauth-back-login"
                        />
                    </>
                )}
                {title && <p className="blong-oauth-callback__brand">{title}</p>}
            </div>
        </div>
    );
}