'use client'; import { useState, useEffect } from 'react'; import type { CustomerOAuthProvider } from 'brainerce'; import { getClient } from '@/core/lib/brainerce'; import { useTranslations } from '@/core/lib/translations'; import { cn } from '@/core/lib/utils'; import { LoadingSpinner } from '@/ui/shared/loading-spinner'; const PROVIDER_CONFIG: Record = { GOOGLE: { labelKey: 'google', icon: ( ), }, FACEBOOK: { labelKey: 'facebook', icon: ( ), }, GITHUB: { labelKey: 'github', icon: ( ), }, }; interface OAuthButtonsProps { className?: string; } export function OAuthButtons({ className }: OAuthButtonsProps) { const t = useTranslations('auth'); const [providers, setProviders] = useState([]); const [loading, setLoading] = useState(true); const [redirecting, setRedirecting] = useState(null); const [failedProvider, setFailedProvider] = useState(null); useEffect(() => { async function fetchProviders() { try { const client = getClient(); const result = await client.getAvailableOAuthProviders(); // `redirectReady` is per storefront address: a provider can be enabled // while the sales channel has nowhere to send the session back to // (TEST channel, no domain, this preview URL not registered). The // authorize call would 400 on click, so render nothing — and tell the // builder why, in dev only, since only the merchant can fix it. // Read through a local shape rather than the SDK type: the scaffold pins // an SDK that may predate the field, and `=== false` keeps an older // backend (which omits it) behaving exactly as before. const readiness = result as { redirectReady?: boolean; redirectBlockedReason?: string }; if (readiness.redirectReady === false) { if (process.env.NODE_ENV !== 'production') { console.warn( `[brainerce] Social login hidden: this storefront's address (${window.location.origin}) ` + `is not registered on the sales channel (reason: ${readiness.redirectBlockedReason}). ` + 'Add it under Dashboard → Sales Channels → Domains.' ); } setProviders([]); return; } setProviders(result.providers); } catch { // OAuth not available - silently hide buttons setProviders([]); } finally { setLoading(false); } } fetchProviders(); }, []); async function handleOAuthClick(provider: CustomerOAuthProvider) { if (redirecting) return; try { setRedirecting(provider); setFailedProvider(null); const client = getClient(); const redirectUrl = window.location.origin + '/api/auth/oauth-callback'; const result = await client.getOAuthAuthorizeUrl(provider, { redirectUrl }); window.location.href = result.authorizationUrl; } catch (err) { // The 400 body is a rule for the merchant (`redirect_not_allowed` and // which channel setting to fix) — the shopper gets store-language copy, // never the English policy text. console.error('OAuth redirect failed:', err); setFailedProvider(provider); setRedirecting(null); } } if (loading) { return null; } if (providers.length === 0) { return null; } return (
{t('orContinueWith')}
{failedProvider && (
{t('socialLoginUnavailable', { provider: t(PROVIDER_CONFIG[failedProvider].labelKey as 'google' | 'facebook' | 'github'), })}
)}
{providers.map((provider) => { const config = PROVIDER_CONFIG[provider]; if (!config) return null; const isRedirecting = redirecting === provider; return ( ); })}
); }