import AppConfig from "@scripts/app/AppConfig"; import { Camera, Canvas, Node, screen, UITransform, Vec3 } from "cc"; import { GoogleImpl, IGoogleBtnLogin, IGoogleLoginRes } from "./GoogleImpl"; interface GoogleCredentialResponse { credential?: string; select_by?: string; } interface GoogleIdentityService { initialize: (config: { client_id: string; callback: (response: GoogleCredentialResponse) => void; ux_mode?: "popup" | "redirect"; }) => void; renderButton: (parent: HTMLElement, options: { 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?: number; }) => void; disableAutoSelect: () => void; } interface GoogleSdkWindow extends Window { google?: { accounts?: { id?: GoogleIdentityService; }; }; } const GOOGLE_SDK_ID = "google-identity-services"; const GOOGLE_SDK_URL = "https://accounts.google.com/gsi/client"; export class GoogleH5Impl extends GoogleImpl { private _initPromise: Promise | null = null; private _loginCallback: (res: IGoogleLoginRes) => void; private _buttonSeed = 0; init(): Promise { if (this.isInitialized) return Promise.resolve(); if (this._initPromise) return this._initPromise; this._initPromise = this.loadGoogleSdk() .then(() => { const googleIdentity = this.getGoogleIdentity(); if (!googleIdentity) { throw new Error("Google Identity Services 初始化失败"); } googleIdentity.initialize({ client_id: AppConfig.GoogleID, ux_mode: "popup", callback: response => this.handleCredentialResponse(response), }); this.onInited(); console.log(googleIdentity); }) .catch(error => { this._initPromise = null; throw error; }); return this._initPromise; } protected async onLogin(): Promise { return null; } async logOut(): Promise { return true; } createLoginBtn(node: Node, callback: (res: IGoogleLoginRes) => void): IGoogleBtnLogin { const container = document.createElement("div"); container.id = `google-login-button-${++this._buttonSeed}`; container.style.position = "fixed"; container.style.zIndex = "9999"; container.style.display = "flex"; container.style.alignItems = "center"; container.style.justifyContent = "center"; container.style.overflow = "hidden"; container.style.boxSizing = "border-box"; container.style.opacity = "0"; document.body.appendChild(container); let active = node.activeInHierarchy; let cleared = false; let animationFrameId = 0; this._loginCallback = callback; const clear = () => { if (cleared) return; cleared = true; window.cancelAnimationFrame(animationFrameId); container.remove(); }; const update = () => { if (cleared) return; if (!node.isValid) { clear(); return; } this.syncDomLayout(container, node, active); animationFrameId = window.requestAnimationFrame(update); }; this.syncDomLayout(container, node, active); animationFrameId = window.requestAnimationFrame(update); this.init() .then(() => { if (cleared || !node.isValid) return; this.syncDomLayout(container, node, active); const containerWidth = Math.round(container.getBoundingClientRect().width) || 280; const width = Math.max(40, Math.min(400, containerWidth)); this.getGoogleIdentity()?.renderButton(container, { type: "standard", theme: "outline", size: "large", text: "signin_with", shape: "rectangular", logo_alignment: "left", width, }); }) .catch(error => { console.error("[Google] 登录按钮创建失败", error); clear(); }); return { active, setActive: (visible: boolean) => { if (cleared) return; if (!node.isValid) { clear(); return; } active = visible; this.syncDomLayout(container, node, active); }, clear, }; } private handleCredentialResponse(response: GoogleCredentialResponse): void { console.log('[Google] handleCredentialResponse:', response, !!this._loginCallback) const credential = response?.credential || ""; const result: IGoogleLoginRes = { // GIS 返回的是 JWT ID Token,沿用项目接口的 accessToken 字段传递。 accessToken: credential, isCancel: !credential, status: credential ? "connected" : "failed", reason: credential ? undefined : "Google 登录未返回 credential", }; this._loginCallback?.(result); } private getGoogleIdentity(): GoogleIdentityService | undefined { return (window as GoogleSdkWindow).google?.accounts?.id; } private loadGoogleSdk(): Promise { if (this.getGoogleIdentity()) return Promise.resolve(); return new Promise((resolve, reject) => { let script = document.getElementById(GOOGLE_SDK_ID) as HTMLScriptElement | null; if (!script) { script = document.createElement("script"); script.id = GOOGLE_SDK_ID; script.src = GOOGLE_SDK_URL; script.async = true; script.defer = true; document.head.appendChild(script); } const clearListeners = () => { window.clearTimeout(timeoutId); script?.removeEventListener("load", onLoad); script?.removeEventListener("error", onError); }; const onLoad = () => { clearListeners(); this.getGoogleIdentity() ? resolve() : reject(new Error("Google Identity Services SDK 不可用")); }; const onError = () => { clearListeners(); script?.remove(); reject(new Error("Google Identity Services SDK 下载失败")); }; const timeoutId = window.setTimeout(() => { clearListeners(); script?.remove(); reject(new Error("Google Identity Services SDK 加载超时")); }, 15000); script.addEventListener("load", onLoad, { once: true }); script.addEventListener("error", onError, { once: true }); }); } private syncDomLayout(container: HTMLDivElement, node: Node, active: boolean): void { if (!active || !node.activeInHierarchy) { container.style.display = "none"; return; } const uiTransform = node.getComponent(UITransform); const camera = this.findCanvasCamera(node); const gameCanvas = this.getGameCanvas(); if (!uiTransform || !camera || !gameCanvas) { container.style.display = "none"; return; } const worldBounds = uiTransform.getBoundingBoxToWorld(); const worldZ = node.worldPosition.z; const bottomLeft = camera.worldToScreen(new Vec3(worldBounds.xMin, worldBounds.yMin, worldZ)); const topRight = camera.worldToScreen(new Vec3(worldBounds.xMax, worldBounds.yMax, worldZ)); const screenWidth = screen.windowSize.width || gameCanvas.width; const screenHeight = screen.windowSize.height || gameCanvas.height; if (screenWidth <= 0 || screenHeight <= 0) { container.style.display = "none"; return; } const canvasRect = gameCanvas.getBoundingClientRect(); const minX = Math.min(bottomLeft.x, topRight.x); const maxX = Math.max(bottomLeft.x, topRight.x); const minY = Math.min(bottomLeft.y, topRight.y); const maxY = Math.max(bottomLeft.y, topRight.y); container.style.display = "flex"; container.style.left = `${canvasRect.left + minX / screenWidth * canvasRect.width}px`; container.style.top = `${canvasRect.top + (1 - maxY / screenHeight) * canvasRect.height}px`; container.style.width = `${(maxX - minX) / screenWidth * canvasRect.width}px`; container.style.height = `${(maxY - minY) / screenHeight * canvasRect.height}px`; } private findCanvasCamera(node: Node): Camera | null { let current: Node | null = node; while (current) { const canvas = current.getComponent(Canvas); if (canvas) return canvas.cameraComponent || current.getComponent(Camera); current = current.parent; } return null; } private getGameCanvas(): HTMLCanvasElement | null { const gameCanvas = document.getElementById("GameCanvas"); if (gameCanvas instanceof HTMLCanvasElement) return gameCanvas; return document.querySelector("canvas"); } }