managarten/packages/shared-auth-ui/src/utils/googleAuth.ts
Till-JS ef70a1af0b Commit Message feat: implement comprehensive shared packages architecture for monorepo SUMMARY: Introduce 10 shared packages to unify common code across all 4 web apps, reducing ~3,000 lines of duplicated code and establishing consistent patterns for authentication, UI components, theming, and utilities. NEW SHARED PACKAGES: - @manacore/shared-auth: Unified auth logic (token management, JWT utils, fetch interceptor, storage/device/network adapters) - @manacore/shared-auth-ui: Reusable auth UI (LoginPage, RegisterPage, OAuth buttons for Google/Apple) - @manacore/shared-tailwind: Unified Tailwind config with 4 themes (lume, nature, stone, ocean) and light/dark mode support - @manacore/shared-icons: Phosphor-based icon library (40+ icons) - @manacore/shared-ui: Atomic design system (Text, Button, Badge, Toggle, Input, Modal) - @manacore/shared-i18n: Unified i18n setup with locale detection - @manacore/shared-config: Environment validation with Zod - @manacore/shared-subscriptio n-types: Subscription type definitions - @manacore/shared-subscriptio n-ui: Subscription UI components (planned) EXTENDED PACKAGES: - @manacore/shared-types: Added auth.ts, theme.ts, ui.ts, common.ts - @manacore/shared-utils: Added format.ts, validation.ts APP MIGRATIONS: - memoro/web: Migrated login (549→46 LOC), tailwind (165→12 LOC), removed 15+ duplicate components - manacore/web: Migrated to client-side auth with shared-auth, added new components (Icon, ThemeToggle, Logo) - manadeck/web: Replaced local authService/tokenManager with shared-auth, migrated auth pages - maerchenzauber/web: Added auth setup, stores, components, routes DELETED FILES (migrated to shared packages): - OAuth buttons (Google/Apple) from memoro, manacore, manadeck - Local authService, tokenManager, deviceManager, jwt utils - Duplicate Modal, Toggle, Text components - iconPaths and ManaIcon components - Subscription-related components (CostCard, PackageCard, etc.) BENEFITS: - 92% reduction in login page code - 93% reduction in tailwind config code - Consistent theming across all apps - Single source of truth for auth logic - Easier maintenance and updates BREAKING CHANGES: - Icon imports now from @manacore/shared-icons - Modal imports from @manacore/shared-ui - OAuth config via setGoogleCl ientId()/setAppleConfig()
2025-11-24 21:09:20 +01:00

174 lines
4.4 KiB
TypeScript

/**
* 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);
});
}