FBNativeImpl.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import { native } from "cc";
  2. import {
  3. FacebookImpl,
  4. IFBEventParams,
  5. IFBLoginRes,
  6. IFBShareOptions,
  7. IFBShareRes,
  8. } from "./FacebookImpl";
  9. const REQUEST_EVENT = "facebook.request";
  10. const RESPONSE_EVENT = "facebook.response";
  11. const DEFAULT_TIMEOUT_MS = 15_000;
  12. const USER_OPERATION_TIMEOUT_MS = 180_000;
  13. interface NativeError {
  14. code?: string;
  15. message?: string;
  16. }
  17. interface NativeResponse<T = Record<string, never>> {
  18. requestId: string;
  19. method: string;
  20. success: boolean;
  21. cancelled?: boolean;
  22. data?: T;
  23. error?: NativeError;
  24. }
  25. interface LoginData {
  26. accessToken?: string;
  27. userId?: string;
  28. name?: string;
  29. authenticationToken?: string;
  30. nonce?: string;
  31. }
  32. interface ShareData {
  33. postId?: string;
  34. }
  35. interface EventData {
  36. accepted?: boolean;
  37. }
  38. interface PendingRequest {
  39. method: string;
  40. resolve: (response: NativeResponse<any>) => void;
  41. reject: (error: Error) => void;
  42. timer: ReturnType<typeof setTimeout>;
  43. }
  44. export class FBNativeImpl extends FacebookImpl {
  45. private _requestSeed = 0;
  46. private _pendingRequests = new Map<string, PendingRequest>();
  47. private _initPromise: Promise<boolean> | null = null;
  48. private readonly _responseListener = (payload: string) => this.onNativeResponse(payload);
  49. init(): void {
  50. if (this._initPromise) return;
  51. native.jsbBridgeWrapper.addNativeEventListener(RESPONSE_EVENT, this._responseListener);
  52. this._initPromise = this.request("init")
  53. .then(() => {
  54. this.onInited();
  55. return true;
  56. })
  57. .catch((error) => {
  58. console.error("[Facebook][Native] 初始化失败:", error);
  59. return false;
  60. });
  61. }
  62. protected async onLogin(): Promise<IFBLoginRes> {
  63. if (!(await this.ensureInitialized())) {
  64. return this.loginFailure("Facebook 原生 SDK 尚未初始化。");
  65. }
  66. try {
  67. const response = await this.request<LoginData>(
  68. "login",
  69. { permissions: ["public_profile", "email"] },
  70. USER_OPERATION_TIMEOUT_MS,
  71. );
  72. if (response.cancelled) {
  73. return {
  74. accessToken: "",
  75. isCancel: true,
  76. status: "cancelled",
  77. };
  78. }
  79. const accessToken = response.data?.accessToken || "";
  80. const authenticationToken = response.data?.authenticationToken || "";
  81. if (!accessToken && !authenticationToken) {
  82. return this.loginFailure("Facebook 登录未返回有效令牌。");
  83. }
  84. return {
  85. accessToken,
  86. userId: response.data?.userId,
  87. name: response.data?.name,
  88. ...(authenticationToken ? {
  89. authenticationToken,
  90. nonce: response.data?.nonce,
  91. tokenType: "authentication" as const,
  92. } : { tokenType: "access" as const }),
  93. isCancel: false,
  94. status: "connected",
  95. };
  96. } catch (error) {
  97. return this.loginFailure(this.errorMessage(error));
  98. }
  99. }
  100. async logOut(): Promise<boolean> {
  101. if (!(await this.ensureInitialized())) return false;
  102. try {
  103. await this.request("logout");
  104. return true;
  105. } catch (error) {
  106. console.error("[Facebook][Native] 登出失败:", error);
  107. return false;
  108. }
  109. }
  110. async share(options: IFBShareOptions): Promise<IFBShareRes> {
  111. if (!(await this.ensureInitialized())) {
  112. return {
  113. success: false,
  114. isCancel: false,
  115. reason: "Facebook 原生 SDK 尚未初始化。",
  116. };
  117. }
  118. try {
  119. const response = await this.request<ShareData>(
  120. "share",
  121. {
  122. url: options?.url || "",
  123. hashtag: options?.hashtag || "",
  124. quote: options?.quote || "",
  125. },
  126. USER_OPERATION_TIMEOUT_MS,
  127. );
  128. return {
  129. success: !response.cancelled,
  130. isCancel: Boolean(response.cancelled),
  131. postId: response.data?.postId,
  132. };
  133. } catch (error) {
  134. return {
  135. success: false,
  136. isCancel: false,
  137. reason: this.errorMessage(error),
  138. };
  139. }
  140. }
  141. async logEvent(
  142. eventName: string,
  143. parameters: IFBEventParams = {},
  144. valueToSum?: number,
  145. ): Promise<boolean> {
  146. if (!(await this.ensureInitialized())) return false;
  147. try {
  148. const params: Record<string, unknown> = { eventName, parameters };
  149. if (valueToSum !== undefined) params.valueToSum = valueToSum;
  150. const response = await this.request<EventData>("logEvent", params);
  151. return response.data?.accepted === true;
  152. } catch (error) {
  153. console.error(`[Facebook][Native] 事件 ${eventName} 记录失败:`, error);
  154. return false;
  155. }
  156. }
  157. async logPurchase(
  158. amount: number,
  159. currency: string,
  160. parameters: IFBEventParams = {},
  161. ): Promise<boolean> {
  162. if (!(await this.ensureInitialized())) return false;
  163. try {
  164. const response = await this.request<EventData>("logPurchase", {
  165. amount,
  166. currency,
  167. parameters,
  168. });
  169. return response.data?.accepted === true;
  170. } catch (error) {
  171. console.error("[Facebook][Native] 购买事件记录失败:", error);
  172. return false;
  173. }
  174. }
  175. async flushEvents(): Promise<boolean> {
  176. if (!(await this.ensureInitialized())) return false;
  177. try {
  178. const response = await this.request<EventData>("flushEvents");
  179. return response.data?.accepted === true;
  180. } catch (error) {
  181. console.error("[Facebook][Native] App Events 刷新失败:", error);
  182. return false;
  183. }
  184. }
  185. private async ensureInitialized(): Promise<boolean> {
  186. if (!this._initPromise) this.init();
  187. return this._initPromise!;
  188. }
  189. private request<T = Record<string, never>>(
  190. method: string,
  191. params: Record<string, unknown> = {},
  192. timeoutMs = DEFAULT_TIMEOUT_MS,
  193. ): Promise<NativeResponse<T>> {
  194. const requestId = `${Date.now()}-${++this._requestSeed}`;
  195. return new Promise<NativeResponse<T>>((resolve, reject) => {
  196. const timer = setTimeout(() => {
  197. this._pendingRequests.delete(requestId);
  198. reject(new Error(`Facebook ${method} 请求超时。`));
  199. }, timeoutMs);
  200. this._pendingRequests.set(requestId, {
  201. method,
  202. resolve,
  203. reject,
  204. timer,
  205. });
  206. try {
  207. native.jsbBridgeWrapper.dispatchEventToNative(
  208. REQUEST_EVENT,
  209. JSON.stringify({ requestId, method, params }),
  210. );
  211. } catch (error) {
  212. clearTimeout(timer);
  213. this._pendingRequests.delete(requestId);
  214. reject(error instanceof Error ? error : new Error(String(error)));
  215. }
  216. });
  217. }
  218. private onNativeResponse(payload: string): void {
  219. let response: NativeResponse<any>;
  220. try {
  221. response = JSON.parse(payload) as NativeResponse<any>;
  222. } catch (error) {
  223. console.error("[Facebook][Native] 无法解析原生响应:", error);
  224. return;
  225. }
  226. const pending = this._pendingRequests.get(response.requestId);
  227. if (!pending) return;
  228. clearTimeout(pending.timer);
  229. this._pendingRequests.delete(response.requestId);
  230. if (response.success) {
  231. pending.resolve(response);
  232. return;
  233. }
  234. const code = response.error?.code ? `[${response.error.code}] ` : "";
  235. pending.reject(new Error(`${code}${response.error?.message || `${pending.method} 执行失败。`}`));
  236. }
  237. private loginFailure(reason: string): IFBLoginRes {
  238. return {
  239. accessToken: "",
  240. isCancel: false,
  241. status: "error",
  242. reason,
  243. };
  244. }
  245. private errorMessage(error: unknown): string {
  246. return error instanceof Error ? error.message : String(error);
  247. }
  248. }