mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 23:16:41 +02:00
refactor(auth): remove all Google/Apple social login code
No external auth providers to keep authentication fully self-sovereign and avoid dependency on third-party services. Removes Google Sign-In, Apple Sign-In components, utilities, endpoints, translations, and mobile dependencies across all apps and shared packages. Google/Apple integrations for data sync (Contacts import, Calendar sync) are intentionally preserved as they serve a different purpose. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
30a0a651ac
commit
2d11ba6248
46 changed files with 499 additions and 2253 deletions
|
|
@ -1,71 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { initializeAppleAuth, signInWithApple, waitForAppleAuth } from '../utils/appleAuth';
|
||||
|
||||
interface Props {
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
let { onError }: Props = $props();
|
||||
|
||||
let isLoading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let sdkLoaded = $state(false);
|
||||
|
||||
async function handleAppleSignIn() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
await signInWithApple();
|
||||
} catch (err) {
|
||||
console.error('Error initiating Apple Sign-In:', err);
|
||||
error = err instanceof Error ? err.message : 'Failed to initiate Apple Sign-In';
|
||||
onError?.(err instanceof Error ? err : new Error('Unknown error during Apple Sign-In'));
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await waitForAppleAuth();
|
||||
const initialized = initializeAppleAuth();
|
||||
if (initialized) {
|
||||
sdkLoaded = true;
|
||||
} else {
|
||||
console.warn('Apple Sign-In not configured - hiding button');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading Apple Sign-In:', err);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if sdkLoaded}
|
||||
<div class="space-y-3">
|
||||
{#if error}
|
||||
<div class="rounded-xl bg-red-500/20 border border-red-500/30 p-3 text-sm text-red-500">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
onclick={handleAppleSignIn}
|
||||
disabled={isLoading}
|
||||
class="flex h-14 w-full items-center justify-center gap-2 rounded-xl bg-black border border-gray-800 px-4 font-medium text-white transition-all hover:bg-gray-900 disabled:opacity-50"
|
||||
>
|
||||
{#if isLoading}
|
||||
<div
|
||||
class="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent"
|
||||
></div>
|
||||
{:else}
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
<span>{isLoading ? 'Signing in...' : 'Continue with Apple'}</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { initializeGoogleAuth, renderGoogleButton, waitForGoogleAuth } from '../utils/googleAuth';
|
||||
|
||||
interface Props {
|
||||
onSuccess: (idToken: string) => Promise<void>;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
let { onSuccess, onError }: Props = $props();
|
||||
|
||||
let buttonContainer: HTMLDivElement;
|
||||
let isLoading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
async function handleGoogleSignIn(idToken: string) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
await onSuccess(idToken);
|
||||
} catch (err) {
|
||||
console.error('Error during Google Sign-In:', err);
|
||||
error = err instanceof Error ? err.message : 'Google Sign-In failed';
|
||||
onError?.(err instanceof Error ? err : new Error('Unknown error during Google Sign-In'));
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await waitForGoogleAuth();
|
||||
initializeGoogleAuth(handleGoogleSignIn);
|
||||
|
||||
if (buttonContainer) {
|
||||
renderGoogleButton(buttonContainer, {
|
||||
type: 'standard',
|
||||
theme: 'outline',
|
||||
size: 'large',
|
||||
text: 'signin_with',
|
||||
shape: 'pill',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error initializing Google Sign-In:', err);
|
||||
error = 'Failed to load Google Sign-In';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<div class="rounded-xl bg-red-500/20 border border-red-500/30 p-3 text-sm text-red-500 mb-2">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
bind:this={buttonContainer}
|
||||
class="relative w-full google-btn-wrapper"
|
||||
style="min-height: 56px;"
|
||||
>
|
||||
{#if isLoading}
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center rounded-xl bg-white/80 dark:bg-black/80 backdrop-blur-sm z-10"
|
||||
>
|
||||
<div
|
||||
class="h-6 w-6 animate-spin rounded-full border-2 border-indigo-500 border-t-transparent"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.google-btn-wrapper > div) {
|
||||
width: 100% !important;
|
||||
height: 56px !important;
|
||||
}
|
||||
|
||||
:global(.google-btn-wrapper iframe) {
|
||||
height: 56px !important;
|
||||
border-radius: 0.75rem !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,34 +4,12 @@ export { default as RegisterPage } from './pages/RegisterPage.svelte';
|
|||
export { default as ForgotPasswordPage } from './pages/ForgotPasswordPage.svelte';
|
||||
|
||||
// Components
|
||||
export { default as GoogleSignInButton } from './components/GoogleSignInButton.svelte';
|
||||
export { default as AppleSignInButton } from './components/AppleSignInButton.svelte';
|
||||
export { default as GuestWelcomeModal } from './components/GuestWelcomeModal.svelte';
|
||||
export { default as AuthGateModal } from './components/AuthGateModal.svelte';
|
||||
export { default as SessionExpiredBanner } from './components/SessionExpiredBanner.svelte';
|
||||
export { default as AuthGate } from './components/AuthGate.svelte';
|
||||
|
||||
// Utilities
|
||||
export {
|
||||
setGoogleClientId,
|
||||
initializeGoogleAuth,
|
||||
renderGoogleButton,
|
||||
isGoogleAuthLoaded,
|
||||
waitForGoogleAuth,
|
||||
} from './utils/googleAuth';
|
||||
|
||||
export {
|
||||
setAppleConfig,
|
||||
initializeAppleAuth,
|
||||
signInWithApple,
|
||||
parseAppleAuthorizationResponse,
|
||||
getStoredReturnUrl,
|
||||
clearAppleSignInSession,
|
||||
isAppleAuthLoaded,
|
||||
waitForAppleAuth,
|
||||
type AppleAuthorizationResponse,
|
||||
} from './utils/appleAuth';
|
||||
|
||||
export {
|
||||
shouldShowGuestWelcome,
|
||||
markGuestWelcomeSeen,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
import type { Component, Snippet } from 'svelte';
|
||||
import type { AuthResult } from '../types';
|
||||
import { Check, Warning, Eye, EyeSlash, SignIn, Sun, Moon } from '@manacore/shared-icons';
|
||||
import GoogleSignInButton from '../components/GoogleSignInButton.svelte';
|
||||
import AppleSignInButton from '../components/AppleSignInButton.svelte';
|
||||
|
||||
/** Translation strings for the login page */
|
||||
export interface LoginTranslations {
|
||||
title: string;
|
||||
|
|
@ -26,9 +23,7 @@
|
|||
emailInvalid: string;
|
||||
passwordRequired: string;
|
||||
signInFailed: string;
|
||||
googleSignInFailed: string;
|
||||
signInSuccess: string;
|
||||
googleSignInSuccess: string;
|
||||
emailVerified?: string;
|
||||
emailNotVerified?: string;
|
||||
resendVerification?: string;
|
||||
|
|
@ -56,9 +51,7 @@
|
|||
emailInvalid: 'Please enter a valid email address',
|
||||
passwordRequired: 'Password is required',
|
||||
signInFailed: 'Sign in failed',
|
||||
googleSignInFailed: 'Google sign in failed',
|
||||
signInSuccess: 'Successfully signed in. Redirecting...',
|
||||
googleSignInSuccess: 'Successfully signed in with Google. Redirecting...',
|
||||
emailVerified: 'Email successfully verified! Please sign in.',
|
||||
emailNotVerified: 'Email not verified.',
|
||||
resendVerification: 'Resend verification email',
|
||||
|
|
@ -71,12 +64,8 @@
|
|||
logo: Component<{ size?: number; color?: string }>;
|
||||
primaryColor: string;
|
||||
onSignIn: (email: string, password: string) => Promise<AuthResult>;
|
||||
onSignInWithGoogle?: (idToken: string) => Promise<AuthResult>;
|
||||
onSignInWithApple?: (identityToken: string) => Promise<AuthResult>;
|
||||
onResendVerification?: (email: string) => Promise<AuthResult>;
|
||||
goto: (path: string) => void;
|
||||
enableGoogle?: boolean;
|
||||
enableApple?: boolean;
|
||||
successRedirect?: string;
|
||||
registerPath?: string;
|
||||
forgotPasswordPath?: string;
|
||||
|
|
@ -102,12 +91,8 @@
|
|||
logo: Logo,
|
||||
primaryColor,
|
||||
onSignIn,
|
||||
onSignInWithGoogle,
|
||||
onSignInWithApple,
|
||||
onResendVerification,
|
||||
goto,
|
||||
enableGoogle = false,
|
||||
enableApple = false,
|
||||
successRedirect = '/dashboard',
|
||||
registerPath = '/register',
|
||||
forgotPasswordPath = '/forgot-password',
|
||||
|
|
@ -269,23 +254,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function handleGoogleSuccess(idToken: string) {
|
||||
if (!onSignInWithGoogle) return;
|
||||
loading = true;
|
||||
clearError();
|
||||
|
||||
const result = await onSignInWithGoogle(idToken);
|
||||
loading = false;
|
||||
|
||||
if (result.success) {
|
||||
showSuccess = true;
|
||||
successAnnouncement = t.googleSignInSuccess;
|
||||
setTimeout(() => goto(successRedirect), 600);
|
||||
} else {
|
||||
setError(result.error || t.googleSignInFailed, 'general');
|
||||
}
|
||||
}
|
||||
|
||||
function skipToForm() {
|
||||
if (emailInput) emailInput.focus();
|
||||
}
|
||||
|
|
@ -519,20 +487,6 @@
|
|||
</button>
|
||||
</form>
|
||||
|
||||
{#if enableGoogle || enableApple}
|
||||
<div class="divider">
|
||||
<span>{t.orDivider}</span>
|
||||
</div>
|
||||
<div class="social-buttons">
|
||||
{#if enableGoogle && onSignInWithGoogle}
|
||||
<GoogleSignInButton onSuccess={handleGoogleSuccess} />
|
||||
{/if}
|
||||
{#if enableApple}
|
||||
<AppleSignInButton />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<p class="register-link">
|
||||
{t.noAccount}
|
||||
<button type="button" onclick={() => goto(registerPath)} style:color={primaryColor}>
|
||||
|
|
@ -973,38 +927,6 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 1.25rem 0;
|
||||
}
|
||||
|
||||
.divider::before,
|
||||
.divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: currentColor;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.divider span {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.light .divider span {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.social-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
|
|
|
|||
|
|
@ -24,21 +24,6 @@ export interface AuthUIConfig {
|
|||
|
||||
/** Redirect path after successful login (default: '/dashboard') */
|
||||
successRedirect?: string;
|
||||
|
||||
/** Enable Google Sign-In */
|
||||
enableGoogle?: boolean;
|
||||
|
||||
/** Enable Apple Sign-In */
|
||||
enableApple?: boolean;
|
||||
|
||||
/** Google OAuth Client ID (required if enableGoogle is true) */
|
||||
googleClientId?: string;
|
||||
|
||||
/** Apple OAuth Service ID (required if enableApple is true) */
|
||||
appleClientId?: string;
|
||||
|
||||
/** Apple OAuth Redirect URI */
|
||||
appleRedirectUri?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -47,8 +32,6 @@ export interface AuthUIConfig {
|
|||
export interface AuthServiceInterface {
|
||||
signIn(email: string, password: string): Promise<AuthResult>;
|
||||
signUp(email: string, password: string): Promise<AuthResult>;
|
||||
signInWithGoogle?(idToken: string): Promise<AuthResult>;
|
||||
signInWithApple?(identityToken: string): Promise<AuthResult>;
|
||||
forgotPassword(email: string): Promise<AuthResult>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
/**
|
||||
* Apple Sign-In integration for web
|
||||
* Uses redirect flow (not popup)
|
||||
*/
|
||||
|
||||
// TypeScript definitions for Apple ID SDK
|
||||
declare global {
|
||||
interface Window {
|
||||
AppleID?: {
|
||||
auth: {
|
||||
init: (config: AppleIDInitConfig) => void;
|
||||
signIn: () => Promise<AppleIDSignInResponse>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface AppleIDInitConfig {
|
||||
clientId: string;
|
||||
scope: string;
|
||||
redirectURI: string;
|
||||
state?: string;
|
||||
nonce?: string;
|
||||
usePopup?: boolean;
|
||||
responseType?: string;
|
||||
responseMode?: string;
|
||||
}
|
||||
|
||||
interface AppleIDSignInResponse {
|
||||
authorization: {
|
||||
code: string;
|
||||
id_token?: string;
|
||||
state?: string;
|
||||
};
|
||||
user?: {
|
||||
email?: string;
|
||||
name?: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppleAuthorizationResponse {
|
||||
code: string;
|
||||
id_token?: string;
|
||||
state?: string;
|
||||
user?: string;
|
||||
}
|
||||
|
||||
let appleClientId: string | null = null;
|
||||
let appleRedirectUri: string | null = null;
|
||||
|
||||
/**
|
||||
* Set Apple Sign-In configuration
|
||||
*/
|
||||
export function setAppleConfig(clientId: string, redirectUri: string) {
|
||||
appleClientId = clientId;
|
||||
appleRedirectUri = redirectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running in browser
|
||||
*/
|
||||
function isBrowser(): boolean {
|
||||
return typeof window !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Apple ID SDK
|
||||
*/
|
||||
export function initializeAppleAuth(): boolean {
|
||||
if (!isBrowser() || !window.AppleID) {
|
||||
console.warn('Apple ID SDK not loaded');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!appleClientId || !appleRedirectUri) {
|
||||
console.error('Apple Sign-In not configured. Call setAppleConfig() first.');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
window.AppleID.auth.init({
|
||||
clientId: appleClientId,
|
||||
scope: 'name email',
|
||||
redirectURI: appleRedirectUri,
|
||||
state: generateState(),
|
||||
usePopup: false,
|
||||
responseType: 'code id_token',
|
||||
responseMode: 'form_post',
|
||||
});
|
||||
|
||||
console.log('Apple ID SDK initialized successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error initializing Apple ID SDK:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate Apple Sign-In (redirect flow)
|
||||
*/
|
||||
export async function signInWithApple(): Promise<void> {
|
||||
if (!isBrowser()) {
|
||||
throw new Error('Apple Sign-In only available in browser');
|
||||
}
|
||||
|
||||
if (!window.AppleID) {
|
||||
throw new Error('Apple ID SDK not loaded');
|
||||
}
|
||||
|
||||
try {
|
||||
const returnTo = window.location.pathname + window.location.search;
|
||||
sessionStorage.setItem('apple_signin_return_to', returnTo);
|
||||
await window.AppleID.auth.signIn();
|
||||
} catch (error) {
|
||||
console.error('Error initiating Apple Sign-In:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Apple authorization response from URL
|
||||
*/
|
||||
export function parseAppleAuthorizationResponse(
|
||||
urlParams: URLSearchParams
|
||||
): AppleAuthorizationResponse | null {
|
||||
const code = urlParams.get('code');
|
||||
const id_token = urlParams.get('id_token');
|
||||
const state = urlParams.get('state');
|
||||
const user = urlParams.get('user');
|
||||
const error = urlParams.get('error');
|
||||
|
||||
if (error) {
|
||||
console.error('Apple Sign-In error:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const storedState = sessionStorage.getItem('apple_signin_state');
|
||||
if (state !== storedState) {
|
||||
console.error('State mismatch - possible CSRF attack');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!id_token && !code) {
|
||||
console.error('No id_token or authorization code in Apple response');
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
code: code || '',
|
||||
id_token: id_token || undefined,
|
||||
state: state || undefined,
|
||||
user: user || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random state for CSRF protection
|
||||
*/
|
||||
function generateState(): string {
|
||||
const state = Math.random().toString(36).substring(2, 15);
|
||||
if (isBrowser()) {
|
||||
sessionStorage.setItem('apple_signin_state', state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored return URL
|
||||
*/
|
||||
export function getStoredReturnUrl(): string {
|
||||
if (!isBrowser()) return '/dashboard';
|
||||
return sessionStorage.getItem('apple_signin_return_to') || '/dashboard';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear Apple Sign-In session data
|
||||
*/
|
||||
export function clearAppleSignInSession() {
|
||||
if (!isBrowser()) return;
|
||||
sessionStorage.removeItem('apple_signin_state');
|
||||
sessionStorage.removeItem('apple_signin_return_to');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Apple ID SDK is loaded
|
||||
*/
|
||||
export function isAppleAuthLoaded(): boolean {
|
||||
return isBrowser() && !!window.AppleID?.auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Apple ID SDK to load
|
||||
*/
|
||||
export function waitForAppleAuth(timeout = 10000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isAppleAuthLoaded()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
if (isAppleAuthLoaded()) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
clearInterval(interval);
|
||||
reject(new Error('Apple ID SDK failed to load'));
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
/**
|
||||
* Google Identity Services integration
|
||||
* Provides helper functions for Google Sign-In on web
|
||||
*/
|
||||
|
||||
// TypeScript definitions for Google Identity Services
|
||||
declare global {
|
||||
interface Window {
|
||||
google?: {
|
||||
accounts: {
|
||||
id: {
|
||||
initialize: (config: GoogleIdConfiguration) => void;
|
||||
prompt: (momentListener?: (notification: PromptMomentNotification) => void) => void;
|
||||
renderButton: (parent: HTMLElement, options: GsiButtonConfiguration) => void;
|
||||
disableAutoSelect: () => void;
|
||||
storeCredential: (credential: { id: string; password: string }) => void;
|
||||
cancel: () => void;
|
||||
onGoogleLibraryLoad: () => void;
|
||||
revoke: (hint: string, callback: (done: RevocationResponse) => void) => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface GoogleIdConfiguration {
|
||||
client_id: string;
|
||||
callback: (response: CredentialResponse) => void;
|
||||
auto_select?: boolean;
|
||||
cancel_on_tap_outside?: boolean;
|
||||
context?: 'signin' | 'signup' | 'use';
|
||||
ux_mode?: 'popup' | 'redirect';
|
||||
login_uri?: string;
|
||||
native_callback?: (response: { id: string; password: string }) => void;
|
||||
itp_support?: boolean;
|
||||
}
|
||||
|
||||
interface CredentialResponse {
|
||||
credential: string;
|
||||
select_by: string;
|
||||
clientId?: string;
|
||||
}
|
||||
|
||||
interface GsiButtonConfiguration {
|
||||
type?: 'standard' | 'icon';
|
||||
theme?: 'outline' | 'filled_blue' | 'filled_black';
|
||||
size?: 'large' | 'medium' | 'small';
|
||||
text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin';
|
||||
shape?: 'rectangular' | 'pill' | 'circle' | 'square';
|
||||
logo_alignment?: 'left' | 'center';
|
||||
width?: string;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
interface PromptMomentNotification {
|
||||
isDisplayMoment: () => boolean;
|
||||
isDisplayed: () => boolean;
|
||||
isNotDisplayed: () => boolean;
|
||||
getNotDisplayedReason: () => string;
|
||||
isSkippedMoment: () => boolean;
|
||||
getSkippedReason: () => string;
|
||||
isDismissedMoment: () => boolean;
|
||||
getDismissedReason: () => string;
|
||||
getMomentType: () => 'display' | 'skipped' | 'dismissed';
|
||||
}
|
||||
|
||||
interface RevocationResponse {
|
||||
successful: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
let googleClientId: string | null = null;
|
||||
|
||||
/**
|
||||
* Set Google Client ID for initialization
|
||||
*/
|
||||
export function setGoogleClientId(clientId: string) {
|
||||
googleClientId = clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Google Identity Services
|
||||
*/
|
||||
export function initializeGoogleAuth(callback: (idToken: string) => void) {
|
||||
if (typeof window === 'undefined') {
|
||||
console.warn('Google Auth: Cannot initialize on server-side');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.google) {
|
||||
console.warn('Google Identity Services not loaded yet');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!googleClientId) {
|
||||
console.error('Google Client ID not configured. Call setGoogleClientId() first.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.google.accounts.id.initialize({
|
||||
client_id: googleClientId,
|
||||
callback: (response: CredentialResponse) => {
|
||||
callback(response.credential);
|
||||
},
|
||||
auto_select: false,
|
||||
cancel_on_tap_outside: true,
|
||||
ux_mode: 'popup',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing Google Auth:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Google Sign-In button
|
||||
*/
|
||||
export function renderGoogleButton(
|
||||
element: HTMLElement,
|
||||
options?: Partial<GsiButtonConfiguration>
|
||||
) {
|
||||
if (typeof window === 'undefined' || !window.google) {
|
||||
console.warn('Google Identity Services not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultOptions: GsiButtonConfiguration = {
|
||||
type: 'standard',
|
||||
theme: 'outline',
|
||||
size: 'large',
|
||||
text: 'signin_with',
|
||||
shape: 'rectangular',
|
||||
logo_alignment: 'left',
|
||||
};
|
||||
|
||||
try {
|
||||
window.google.accounts.id.renderButton(element, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error rendering Google button:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Google Identity Services is loaded
|
||||
*/
|
||||
export function isGoogleAuthLoaded(): boolean {
|
||||
return typeof window !== 'undefined' && !!window.google?.accounts?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Google Identity Services to load
|
||||
*/
|
||||
export function waitForGoogleAuth(timeout = 10000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isGoogleAuthLoaded()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
if (isGoogleAuthLoaded()) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
clearInterval(interval);
|
||||
reject(new Error('Google Identity Services failed to load'));
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue