|
|
@@ -1,15 +1,267 @@
|
|
|
-import { FacebookImpl } from "./FacebookImpl";
|
|
|
+import { native } from "cc";
|
|
|
+import {
|
|
|
+ FacebookImpl,
|
|
|
+ IFBEventParams,
|
|
|
+ IFBLoginRes,
|
|
|
+ IFBShareOptions,
|
|
|
+ IFBShareRes,
|
|
|
+} from "./FacebookImpl";
|
|
|
+
|
|
|
+const REQUEST_EVENT = "facebook.request";
|
|
|
+const RESPONSE_EVENT = "facebook.response";
|
|
|
+const DEFAULT_TIMEOUT_MS = 15_000;
|
|
|
+const USER_OPERATION_TIMEOUT_MS = 180_000;
|
|
|
+
|
|
|
+interface NativeError {
|
|
|
+ code?: string;
|
|
|
+ message?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface NativeResponse<T = Record<string, never>> {
|
|
|
+ requestId: string;
|
|
|
+ method: string;
|
|
|
+ success: boolean;
|
|
|
+ cancelled?: boolean;
|
|
|
+ data?: T;
|
|
|
+ error?: NativeError;
|
|
|
+}
|
|
|
+
|
|
|
+interface LoginData {
|
|
|
+ accessToken?: string;
|
|
|
+ userId?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface ShareData {
|
|
|
+ postId?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface EventData {
|
|
|
+ accepted?: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+interface PendingRequest {
|
|
|
+ method: string;
|
|
|
+ resolve: (response: NativeResponse<any>) => void;
|
|
|
+ reject: (error: Error) => void;
|
|
|
+ timer: ReturnType<typeof setTimeout>;
|
|
|
+}
|
|
|
|
|
|
export class FBAndroidImpl extends FacebookImpl {
|
|
|
- init() {
|
|
|
+ private _requestSeed = 0;
|
|
|
+ private _pendingRequests = new Map<string, PendingRequest>();
|
|
|
+ private _initPromise: Promise<boolean> | null = null;
|
|
|
+ private readonly _responseListener = (payload: string) => this.onNativeResponse(payload);
|
|
|
+
|
|
|
+ init(): void {
|
|
|
+ if (this._initPromise) return;
|
|
|
+
|
|
|
+ native.jsbBridgeWrapper.addNativeEventListener(RESPONSE_EVENT, this._responseListener);
|
|
|
+ this._initPromise = this.request("init")
|
|
|
+ .then(() => {
|
|
|
+ this.onInited();
|
|
|
+ return true;
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ console.error("[Facebook][Android] 初始化失败:", error);
|
|
|
+ return false;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ protected async onLogin(): Promise<IFBLoginRes> {
|
|
|
+ if (!(await this.ensureInitialized())) {
|
|
|
+ return this.loginFailure("Facebook Android SDK 尚未初始化。");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await this.request<LoginData>(
|
|
|
+ "login",
|
|
|
+ { permissions: ["public_profile", "email"] },
|
|
|
+ USER_OPERATION_TIMEOUT_MS,
|
|
|
+ );
|
|
|
+ if (response.cancelled) {
|
|
|
+ return {
|
|
|
+ accessToken: "",
|
|
|
+ isCancel: true,
|
|
|
+ status: "cancelled",
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const accessToken = response.data?.accessToken || "";
|
|
|
+ if (!accessToken) {
|
|
|
+ return this.loginFailure("Facebook 登录未返回 Access Token。");
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ accessToken,
|
|
|
+ isCancel: false,
|
|
|
+ status: "connected",
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ return this.loginFailure(this.errorMessage(error));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async logOut(): Promise<boolean> {
|
|
|
+ if (!(await this.ensureInitialized())) return false;
|
|
|
+ try {
|
|
|
+ await this.request("logout");
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error("[Facebook][Android] 登出失败:", error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async share(options: IFBShareOptions): Promise<IFBShareRes> {
|
|
|
+ if (!(await this.ensureInitialized())) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ isCancel: false,
|
|
|
+ reason: "Facebook Android SDK 尚未初始化。",
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await this.request<ShareData>(
|
|
|
+ "share",
|
|
|
+ {
|
|
|
+ url: options?.url || "",
|
|
|
+ hashtag: options?.hashtag || "",
|
|
|
+ quote: options?.quote || "",
|
|
|
+ },
|
|
|
+ USER_OPERATION_TIMEOUT_MS,
|
|
|
+ );
|
|
|
+ return {
|
|
|
+ success: !response.cancelled,
|
|
|
+ isCancel: Boolean(response.cancelled),
|
|
|
+ postId: response.data?.postId,
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ isCancel: false,
|
|
|
+ reason: this.errorMessage(error),
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async logEvent(
|
|
|
+ eventName: string,
|
|
|
+ parameters: IFBEventParams = {},
|
|
|
+ valueToSum?: number,
|
|
|
+ ): Promise<boolean> {
|
|
|
+ if (!(await this.ensureInitialized())) return false;
|
|
|
+ try {
|
|
|
+ const params: Record<string, unknown> = { eventName, parameters };
|
|
|
+ if (valueToSum !== undefined) params.valueToSum = valueToSum;
|
|
|
+ const response = await this.request<EventData>("logEvent", params);
|
|
|
+ return response.data?.accepted === true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`[Facebook][Android] 事件 ${eventName} 记录失败:`, error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async logPurchase(
|
|
|
+ amount: number,
|
|
|
+ currency: string,
|
|
|
+ parameters: IFBEventParams = {},
|
|
|
+ ): Promise<boolean> {
|
|
|
+ if (!(await this.ensureInitialized())) return false;
|
|
|
+ try {
|
|
|
+ const response = await this.request<EventData>("logPurchase", {
|
|
|
+ amount,
|
|
|
+ currency,
|
|
|
+ parameters,
|
|
|
+ });
|
|
|
+ return response.data?.accepted === true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error("[Facebook][Android] 购买事件记录失败:", error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async flushEvents(): Promise<boolean> {
|
|
|
+ if (!(await this.ensureInitialized())) return false;
|
|
|
+ try {
|
|
|
+ const response = await this.request<EventData>("flushEvents");
|
|
|
+ return response.data?.accepted === true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error("[Facebook][Android] App Events 刷新失败:", error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async ensureInitialized(): Promise<boolean> {
|
|
|
+ if (!this._initPromise) this.init();
|
|
|
+ return this._initPromise!;
|
|
|
+ }
|
|
|
+
|
|
|
+ private request<T = Record<string, never>>(
|
|
|
+ method: string,
|
|
|
+ params: Record<string, unknown> = {},
|
|
|
+ timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
|
+ ): Promise<NativeResponse<T>> {
|
|
|
+ const requestId = `${Date.now()}-${++this._requestSeed}`;
|
|
|
+ return new Promise<NativeResponse<T>>((resolve, reject) => {
|
|
|
+ const timer = setTimeout(() => {
|
|
|
+ this._pendingRequests.delete(requestId);
|
|
|
+ reject(new Error(`Facebook ${method} 请求超时。`));
|
|
|
+ }, timeoutMs);
|
|
|
+
|
|
|
+ this._pendingRequests.set(requestId, {
|
|
|
+ method,
|
|
|
+ resolve,
|
|
|
+ reject,
|
|
|
+ timer,
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ native.jsbBridgeWrapper.dispatchEventToNative(
|
|
|
+ REQUEST_EVENT,
|
|
|
+ JSON.stringify({ requestId, method, params }),
|
|
|
+ );
|
|
|
+ } catch (error) {
|
|
|
+ clearTimeout(timer);
|
|
|
+ this._pendingRequests.delete(requestId);
|
|
|
+ reject(error instanceof Error ? error : new Error(String(error)));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private onNativeResponse(payload: string): void {
|
|
|
+ let response: NativeResponse<any>;
|
|
|
+ try {
|
|
|
+ response = JSON.parse(payload) as NativeResponse<any>;
|
|
|
+ } catch (error) {
|
|
|
+ console.error("[Facebook][Android] 无法解析原生响应:", error);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const pending = this._pendingRequests.get(response.requestId);
|
|
|
+ if (!pending) return;
|
|
|
+
|
|
|
+ clearTimeout(pending.timer);
|
|
|
+ this._pendingRequests.delete(response.requestId);
|
|
|
+ if (response.success) {
|
|
|
+ pending.resolve(response);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
+ const code = response.error?.code ? `[${response.error.code}] ` : "";
|
|
|
+ pending.reject(new Error(`${code}${response.error?.message || `${pending.method} 执行失败。`}`));
|
|
|
}
|
|
|
|
|
|
- protected async onLogin() {
|
|
|
- return { accessToken: "", isCancel: false };
|
|
|
+ private loginFailure(reason: string): IFBLoginRes {
|
|
|
+ return {
|
|
|
+ accessToken: "",
|
|
|
+ isCancel: false,
|
|
|
+ status: "error",
|
|
|
+ reason,
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- async logOut() {
|
|
|
- return true;
|
|
|
+ private errorMessage(error: unknown): string {
|
|
|
+ return error instanceof Error ? error.message : String(error);
|
|
|
}
|
|
|
-}
|
|
|
+}
|