FBNativeImpl.ts 8.4 KB

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