/** * Player Identifier Management * * Generates and manages a unique player identifier (UUID) that is stored * in localStorage. This identifier is used to sync game states across * different domains (hoerdle.de and hördle.de). * * Device-specific isolation: * - Each device has its own device ID stored in localStorage * - Player ID format: {basePlayerId}:{deviceId} * - This allows cross-domain sync on the same device while keeping devices isolated */ const STORAGE_KEY_PLAYER = 'hoerdle_player_id'; const STORAGE_KEY_DEVICE = 'hoerdle_device_id'; /** * Generate a UUID v4 */ function generateUUID(): string { // UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /** * Get or create a device ID (unique per device) * * The device ID is stored in localStorage and persists across sessions. * This allows device-specific isolation of game states. * * @returns Device identifier (UUID v4) */ export function getOrCreateDeviceId(): string { if (typeof window === 'undefined') { return ''; } let deviceId = localStorage.getItem(STORAGE_KEY_DEVICE); if (!deviceId) { deviceId = generateUUID(); localStorage.setItem(STORAGE_KEY_DEVICE, deviceId); } return deviceId; } /** * Get the device ID without creating a new one * * @returns Device identifier or null if not set */ export function getDeviceId(): string | null { if (typeof window === 'undefined') { return null; } return localStorage.getItem(STORAGE_KEY_DEVICE); } /** * Generate a base player ID (for cross-domain sync) */ function generateBasePlayerId(): string { return generateUUID(); } /** * Try to find an existing base player ID from the backend * * Extracts the base player ID from a full player ID (format: {basePlayerId}:{deviceId}) * * @param genreKey - Genre key to search for * @returns Base player ID if found, null otherwise */ async function findExistingBasePlayerId(genreKey: string): Promise { try { const deviceId = getOrCreateDeviceId(); const response = await fetch('/api/player-id/suggest', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ genreKey, deviceId }), }); if (response.ok) { const data = await response.json(); if (data.basePlayerId) { return data.basePlayerId; } } } catch (error) { console.warn('[playerId] Failed to find existing base player ID:', error); } return null; } /** * Combine base player ID and device ID into full player ID * Format: {basePlayerId}:{deviceId} */ function combinePlayerId(basePlayerId: string, deviceId: string): string { return `${basePlayerId}:${deviceId}`; } /** * Extract base player ID from full player ID * Format: {basePlayerId}:{deviceId} -> {basePlayerId} */ function extractBasePlayerId(fullPlayerId: string): string { const colonIndex = fullPlayerId.indexOf(':'); if (colonIndex === -1) { // Legacy format (no device ID) - return as is return fullPlayerId; } return fullPlayerId.substring(0, colonIndex); } /** * Get or create a player identifier * * Player ID format: {basePlayerId}:{deviceId} * * If no identifier exists in localStorage, tries to find an existing base player ID * from the backend (for cross-domain sync). If none found, generates a new base ID. * The device ID is always device-specific. * * This enables: * - Cross-domain synchronization on the same device (same base player ID) * - Device isolation (different device IDs) * * @param genreKey - Optional genre key to search for existing base player ID * @returns Full player identifier ({basePlayerId}:{deviceId}) */ export async function getOrCreatePlayerIdAsync(genreKey?: string): Promise { if (typeof window === 'undefined') { return ''; } // Always get/create device ID (device-specific) const deviceId = getOrCreateDeviceId(); // Try to get base player ID from localStorage let basePlayerId = localStorage.getItem(STORAGE_KEY_PLAYER); if (!basePlayerId) { // Try to find an existing base player ID from backend if genreKey is provided if (genreKey) { const existingBaseId = await findExistingBasePlayerId(genreKey); if (existingBaseId) { basePlayerId = existingBaseId; localStorage.setItem(STORAGE_KEY_PLAYER, basePlayerId); } } // Generate new base player ID if no existing one found if (!basePlayerId) { basePlayerId = generateBasePlayerId(); localStorage.setItem(STORAGE_KEY_PLAYER, basePlayerId); } } // Combine base player ID with device ID return combinePlayerId(basePlayerId, deviceId); } /** * Get or create a player identifier (synchronous version) * * This is the legacy synchronous version. For cross-domain sync, use getOrCreatePlayerIdAsync instead. * * @returns Full player identifier ({basePlayerId}:{deviceId}) */ export function getOrCreatePlayerId(): string { if (typeof window === 'undefined') { return ''; } const deviceId = getOrCreateDeviceId(); let basePlayerId = localStorage.getItem(STORAGE_KEY_PLAYER); if (!basePlayerId) { basePlayerId = generateBasePlayerId(); localStorage.setItem(STORAGE_KEY_PLAYER, basePlayerId); } return combinePlayerId(basePlayerId, deviceId); } /** * Get the current player identifier without creating a new one * * @returns Full player identifier ({basePlayerId}:{deviceId}) or null if not set */ export function getPlayerId(): string | null { if (typeof window === 'undefined') { return null; } const deviceId = getDeviceId(); const basePlayerId = localStorage.getItem(STORAGE_KEY_PLAYER); if (!deviceId || !basePlayerId) { return null; } return combinePlayerId(basePlayerId, deviceId); } /** * Get base player ID (for debugging/logging) * * @returns Base player ID or null if not set */ export function getBasePlayerId(): string | null { if (typeof window === 'undefined') { return null; } return localStorage.getItem(STORAGE_KEY_PLAYER); }