GoogleH5Impl.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { Camera, Canvas, Node, screen, UITransform, Vec3 } from "cc";
  2. import { GoogleImpl, IGoogleBtnLogin, IGoogleLoginRes } from "./GoogleImpl";
  3. import AppConfig from "@scripts/app/AppConfig";
  4. interface GoogleCredentialResponse {
  5. credential?: string;
  6. select_by?: string;
  7. }
  8. interface GoogleIdentityService {
  9. initialize: (config: {
  10. client_id: string;
  11. callback: (response: GoogleCredentialResponse) => void;
  12. ux_mode?: "popup" | "redirect";
  13. }) => void;
  14. renderButton: (parent: HTMLElement, options: {
  15. type?: "standard" | "icon";
  16. theme?: "outline" | "filled_blue" | "filled_black";
  17. size?: "large" | "medium" | "small";
  18. text?: "signin_with" | "signup_with" | "continue_with" | "signin";
  19. shape?: "rectangular" | "pill" | "circle" | "square";
  20. logo_alignment?: "left" | "center";
  21. width?: number;
  22. }) => void;
  23. disableAutoSelect: () => void;
  24. }
  25. interface GoogleSdkWindow extends Window {
  26. google?: {
  27. accounts?: {
  28. id?: GoogleIdentityService;
  29. };
  30. };
  31. }
  32. const GOOGLE_SDK_ID = "google-identity-services";
  33. const GOOGLE_SDK_URL = "https://accounts.google.com/gsi/client";
  34. // const GOOGLE_CLIENTID = "984811369314-r66m16sppu58oej4lji56bgnernamhut.apps.googleusercontent.com";
  35. export class GoogleH5Impl extends GoogleImpl {
  36. private _initPromise: Promise<void> | null = null;
  37. private _loginCallback: (res: IGoogleLoginRes) => void;
  38. private _buttonSeed = 0;
  39. init(): Promise<void> {
  40. if (this.isInitialized) return Promise.resolve();
  41. if (this._initPromise) return this._initPromise;
  42. this._initPromise = this.loadGoogleSdk()
  43. .then(() => {
  44. const googleIdentity = this.getGoogleIdentity();
  45. if (!googleIdentity) {
  46. console.error("Google Identity Services 初始化失败");
  47. return;
  48. }
  49. googleIdentity.initialize({
  50. client_id: AppConfig.GoogleID,
  51. ux_mode: "popup",
  52. callback: response => this.handleCredentialResponse(response),
  53. });
  54. this.onInited();
  55. console.log(googleIdentity);
  56. })
  57. .catch(error => {
  58. this._initPromise = null;
  59. console.error(error);
  60. });
  61. return this._initPromise;
  62. }
  63. protected async onLogin(): Promise<IGoogleLoginRes> {
  64. return null;
  65. }
  66. async logOut(): Promise<boolean> {
  67. return true;
  68. }
  69. createLoginBtn(node: Node, callback: (res: IGoogleLoginRes) => void): IGoogleBtnLogin {
  70. const container = document.createElement("div");
  71. container.id = `google-login-button-${++this._buttonSeed}`;
  72. container.style.position = "fixed";
  73. container.style.zIndex = "9999";
  74. container.style.display = "flex";
  75. container.style.alignItems = "center";
  76. container.style.justifyContent = "center";
  77. container.style.overflow = "hidden";
  78. container.style.boxSizing = "border-box";
  79. container.style.opacity = "0";
  80. document.body.appendChild(container);
  81. let active = node.activeInHierarchy;
  82. let cleared = false;
  83. let animationFrameId = 0;
  84. this._loginCallback = callback;
  85. const clear = () => {
  86. if (cleared) return;
  87. cleared = true;
  88. window.cancelAnimationFrame(animationFrameId);
  89. container.remove();
  90. };
  91. const update = () => {
  92. if (cleared) return;
  93. if (!node.isValid) {
  94. clear();
  95. return;
  96. }
  97. this.syncDomLayout(container, node, active);
  98. animationFrameId = window.requestAnimationFrame(update);
  99. };
  100. this.syncDomLayout(container, node, active);
  101. animationFrameId = window.requestAnimationFrame(update);
  102. this.init()
  103. .then(() => {
  104. if (cleared || !node.isValid) return;
  105. this.syncDomLayout(container, node, active);
  106. const containerWidth = Math.round(container.getBoundingClientRect().width) || 280;
  107. const width = Math.max(40, Math.min(400, containerWidth));
  108. this.getGoogleIdentity()?.renderButton(container, {
  109. type: "standard",
  110. theme: "outline",
  111. size: "large",
  112. text: "signin_with",
  113. shape: "rectangular",
  114. logo_alignment: "left",
  115. width,
  116. });
  117. })
  118. .catch(error => {
  119. console.error("[Google] 登录按钮创建失败", error);
  120. clear();
  121. });
  122. return {
  123. active,
  124. setActive: (visible: boolean) => {
  125. if (cleared) return;
  126. if (!node.isValid) {
  127. clear();
  128. return;
  129. }
  130. active = visible;
  131. this.syncDomLayout(container, node, active);
  132. },
  133. clear,
  134. };
  135. }
  136. private handleCredentialResponse(response: GoogleCredentialResponse): void {
  137. console.log('[Google] handleCredentialResponse:', response, !!this._loginCallback)
  138. const credential = response?.credential || "";
  139. const result: IGoogleLoginRes = {
  140. // GIS 返回的是 JWT ID Token,沿用项目接口的 accessToken 字段传递。
  141. accessToken: credential,
  142. isCancel: !credential,
  143. status: credential ? "connected" : "failed",
  144. reason: credential ? undefined : "Google 登录未返回 credential",
  145. };
  146. this._loginCallback?.(result);
  147. }
  148. private getGoogleIdentity(): GoogleIdentityService | undefined {
  149. return (window as GoogleSdkWindow).google?.accounts?.id;
  150. }
  151. private loadGoogleSdk(): Promise<void> {
  152. if (this.getGoogleIdentity()) return Promise.resolve();
  153. return new Promise((resolve, reject) => {
  154. let script = document.getElementById(GOOGLE_SDK_ID) as HTMLScriptElement | null;
  155. if (!script) {
  156. script = document.createElement("script");
  157. script.id = GOOGLE_SDK_ID;
  158. script.src = GOOGLE_SDK_URL;
  159. script.async = true;
  160. script.defer = true;
  161. document.head.appendChild(script);
  162. }
  163. const clearListeners = () => {
  164. window.clearTimeout(timeoutId);
  165. script?.removeEventListener("load", onLoad);
  166. script?.removeEventListener("error", onError);
  167. };
  168. const onLoad = () => {
  169. clearListeners();
  170. const id = this.getGoogleIdentity();
  171. id ? resolve() : reject();
  172. if (!id) console.error("Google Identity Services SDK 不可用");
  173. };
  174. const onError = () => {
  175. clearListeners();
  176. script?.remove();
  177. reject();
  178. console.error("Google Identity Services SDK 下载失败");
  179. };
  180. const timeoutId = window.setTimeout(() => {
  181. clearListeners();
  182. script?.remove();
  183. reject();
  184. console.error("Google Identity Services SDK 加载超时");
  185. }, 15000);
  186. script.addEventListener("load", onLoad, { once: true });
  187. script.addEventListener("error", onError, { once: true });
  188. });
  189. }
  190. private syncDomLayout(container: HTMLDivElement, node: Node, active: boolean): void {
  191. if (!active || !node.activeInHierarchy) {
  192. container.style.display = "none";
  193. return;
  194. }
  195. const uiTransform = node.getComponent(UITransform);
  196. const camera = this.findCanvasCamera(node);
  197. const gameCanvas = this.getGameCanvas();
  198. if (!uiTransform || !camera || !gameCanvas) {
  199. container.style.display = "none";
  200. return;
  201. }
  202. const worldBounds = uiTransform.getBoundingBoxToWorld();
  203. const worldZ = node.worldPosition.z;
  204. const bottomLeft = camera.worldToScreen(new Vec3(worldBounds.xMin, worldBounds.yMin, worldZ));
  205. const topRight = camera.worldToScreen(new Vec3(worldBounds.xMax, worldBounds.yMax, worldZ));
  206. const screenWidth = screen.windowSize.width || gameCanvas.width;
  207. const screenHeight = screen.windowSize.height || gameCanvas.height;
  208. if (screenWidth <= 0 || screenHeight <= 0) {
  209. container.style.display = "none";
  210. return;
  211. }
  212. const canvasRect = gameCanvas.getBoundingClientRect();
  213. const minX = Math.min(bottomLeft.x, topRight.x);
  214. const maxX = Math.max(bottomLeft.x, topRight.x);
  215. const minY = Math.min(bottomLeft.y, topRight.y);
  216. const maxY = Math.max(bottomLeft.y, topRight.y);
  217. container.style.display = "flex";
  218. container.style.left = `${canvasRect.left + minX / screenWidth * canvasRect.width}px`;
  219. container.style.top = `${canvasRect.top + (1 - maxY / screenHeight) * canvasRect.height}px`;
  220. container.style.width = `${(maxX - minX) / screenWidth * canvasRect.width}px`;
  221. container.style.height = `${(maxY - minY) / screenHeight * canvasRect.height}px`;
  222. }
  223. private findCanvasCamera(node: Node): Camera | null {
  224. let current: Node | null = node;
  225. while (current) {
  226. const canvas = current.getComponent(Canvas);
  227. if (canvas) return canvas.cameraComponent || current.getComponent(Camera);
  228. current = current.parent;
  229. }
  230. return null;
  231. }
  232. private getGameCanvas(): HTMLCanvasElement | null {
  233. const gameCanvas = document.getElementById("GameCanvas");
  234. if (gameCanvas instanceof HTMLCanvasElement) return gameCanvas;
  235. return document.querySelector("canvas");
  236. }
  237. }