GoogleNativeImpl.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import { native } from "cc";
  2. import { GoogleImpl, IGoogleLoginRes } from "./GoogleImpl";
  3. import AppConfig from "@scripts/app/AppConfig";
  4. const REQUEST_EVENT = "google.request";
  5. const RESPONSE_EVENT = "google.response";
  6. const DEFAULT_TIMEOUT_MS = 15_000;
  7. const LOGIN_TIMEOUT_MS = 180_000;
  8. // const GOOGLE_CLIENTID = "984811369314-tgai3uj7lb3dmvpuisehm7fb98o60vsq.apps.googleusercontent.com";
  9. interface NativeResponse<T = Record<string, never>> {
  10. requestId: string;
  11. method: string;
  12. success: boolean;
  13. cancelled?: boolean;
  14. data?: T;
  15. error?: {
  16. code?: string;
  17. message?: string;
  18. };
  19. }
  20. interface GoogleLoginData {
  21. userId?: string;
  22. idToken?: string;
  23. email?: string;
  24. displayName?: string;
  25. givenName?: string;
  26. familyName?: string;
  27. profilePictureUrl?: string;
  28. }
  29. interface PendingRequest {
  30. method: string;
  31. resolve: (response: NativeResponse<any>) => void;
  32. reject: (error: Error) => void;
  33. timer: ReturnType<typeof setTimeout>;
  34. }
  35. export class GoogleNativeImpl extends GoogleImpl {
  36. private _requestSeed = 0;
  37. private _pendingRequests = new Map<string, PendingRequest>();
  38. private _initPromise: Promise<boolean> | null = null;
  39. private readonly _responseListener = (payload: string) => this.onNativeResponse(payload);
  40. init(): void {
  41. if (this._initPromise) return;
  42. native.jsbBridgeWrapper.addNativeEventListener(RESPONSE_EVENT, this._responseListener);
  43. this._initPromise = this.request("init", { webClientId: AppConfig.GoogleID })
  44. .then(() => {
  45. this.onInited();
  46. return true;
  47. })
  48. .catch((error) => {
  49. console.error("[Google][Android] 初始化失败:", error);
  50. return false;
  51. });
  52. }
  53. protected async onLogin(): Promise<IGoogleLoginRes> {
  54. if (!(await this.ensureInitialized())) {
  55. return this.loginFailure("Google 原生登录尚未初始化,请检查 Client ID 配置。");
  56. }
  57. try {
  58. const response = await this.request<GoogleLoginData>(
  59. "login",
  60. {},
  61. LOGIN_TIMEOUT_MS,
  62. );
  63. if (response.cancelled) {
  64. return {
  65. accessToken: "",
  66. isCancel: true,
  67. status: "cancelled",
  68. };
  69. }
  70. const idToken = response.data?.idToken || "";
  71. if (!idToken) {
  72. return this.loginFailure("Google 登录未返回 ID Token。");
  73. }
  74. return {
  75. // 与 H5 保持一致:accessToken 字段承载需要交给服务端验证的 Google ID Token。
  76. accessToken: idToken,
  77. userId: response.data?.userId,
  78. name: response.data?.displayName,
  79. email: response.data?.email,
  80. isCancel: false,
  81. status: "connected",
  82. };
  83. } catch (error) {
  84. return this.loginFailure(this.errorMessage(error));
  85. }
  86. }
  87. async logOut(): Promise<boolean> {
  88. if (!(await this.ensureInitialized())) return false;
  89. try {
  90. await this.request("logout");
  91. return true;
  92. } catch (error) {
  93. console.error("[Google][Android] 登出失败:", error);
  94. return false;
  95. }
  96. }
  97. private async ensureInitialized(): Promise<boolean> {
  98. if (!this._initPromise) this.init();
  99. return this._initPromise!;
  100. }
  101. private request<T = Record<string, never>>(
  102. method: string,
  103. params: Record<string, unknown> = {},
  104. timeoutMs = DEFAULT_TIMEOUT_MS,
  105. ): Promise<NativeResponse<T>> {
  106. const requestId = `${Date.now()}-${++this._requestSeed}`;
  107. return new Promise<NativeResponse<T>>((resolve, reject) => {
  108. const timer = setTimeout(() => {
  109. this._pendingRequests.delete(requestId);
  110. reject(new Error(`Google ${method} 请求超时。`));
  111. }, timeoutMs);
  112. this._pendingRequests.set(requestId, {
  113. method,
  114. resolve,
  115. reject,
  116. timer,
  117. });
  118. try {
  119. native.jsbBridgeWrapper.dispatchEventToNative(
  120. REQUEST_EVENT,
  121. JSON.stringify({ requestId, method, params }),
  122. );
  123. } catch (error) {
  124. clearTimeout(timer);
  125. this._pendingRequests.delete(requestId);
  126. reject(error instanceof Error ? error : new Error(String(error)));
  127. }
  128. });
  129. }
  130. private onNativeResponse(payload: string): void {
  131. let response: NativeResponse<any>;
  132. try {
  133. response = JSON.parse(payload) as NativeResponse<any>;
  134. } catch (error) {
  135. console.error("[Google][Android] 无法解析原生响应:", error);
  136. return;
  137. }
  138. const pending = this._pendingRequests.get(response.requestId);
  139. if (!pending) return;
  140. clearTimeout(pending.timer);
  141. this._pendingRequests.delete(response.requestId);
  142. if (response.success) {
  143. pending.resolve(response);
  144. return;
  145. }
  146. const code = response.error?.code ? `[${response.error.code}] ` : "";
  147. pending.reject(new Error(`${code}${response.error?.message || `${pending.method} 执行失败。`}`));
  148. }
  149. private loginFailure(reason: string): IGoogleLoginRes {
  150. return {
  151. accessToken: "",
  152. isCancel: false,
  153. status: "error",
  154. reason,
  155. };
  156. }
  157. private errorMessage(error: unknown): string {
  158. return error instanceof Error ? error.message : String(error);
  159. }
  160. }