#import "FBWrapper.h" // Swift-generated headers need these protocol definitions when compiled as Objective-C++. #import #import #import #import #import #import #import #import #import "platform/apple/JsbBridgeWrapper.h" #include "application/ApplicationManager.h" #include "application/BaseApplication.h" #include "base/Scheduler.h" #include "engine/BaseEngine.h" #include @interface FBWrapper () @property(nonatomic, strong) FBSDKLoginManager *loginManager; @property(nonatomic, strong) FBSDKShareDialog *shareDialog; @property(nonatomic, strong) NSDictionary *shareRequest; @property(nonatomic, copy) NSString *startupError; @property(nonatomic) BOOL loginPending; @end @implementation FBWrapper + (instancetype)shared { static FBWrapper *instance; static dispatch_once_t once; dispatch_once(&once, ^{ instance = [FBWrapper new]; }); return instance; } - (void)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)options { // Register even on configuration failure so JS receives a useful error instead of timing out. // Creator 3.8.8's MRC bridge releases the listener after adding it to its array. // Keep this outer block capture-free (no self or local variables): a global block // has static lifetime and is safe with that ownership behavior. GCD owns the // inner block and retains its payload until the main-queue callback runs. static OnScriptEventListener const requestListener = ^(NSString *payload) { dispatch_async(dispatch_get_main_queue(), ^{ [[FBWrapper shared] handlePayload:payload]; }); }; [[JsbBridgeWrapper sharedInstance] addScriptEventListener:@"facebook.request" listener:requestListener]; NSDictionary *info = NSBundle.mainBundle.infoDictionary; if (![info[@"FacebookAppID"] isKindOfClass:NSString.class] || ![info[@"FacebookAppID"] length] || ![info[@"FacebookClientToken"] isKindOfClass:NSString.class] || ![info[@"FacebookClientToken"] length]) { self.startupError = @"请在 native/engine/ios/Info.plist 中填写 FacebookAppID 和 FacebookClientToken。"; return; } @try { [[FBSDKApplicationDelegate sharedInstance] application:application didFinishLaunchingWithOptions:options]; self.loginManager = [FBSDKLoginManager new]; } @catch (NSException *exception) { self.startupError = exception.reason ?: @"Facebook 初始化失败。"; } } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { if (!self.loginManager) return NO; return [[FBSDKApplicationDelegate sharedInstance] application:application openURL:url options:options]; } - (void)reply:(NSDictionary *)request data:(NSDictionary *)data cancelled:(BOOL)cancelled error:(NSString *)error { NSMutableDictionary *response = [@{@"requestId": request[@"requestId"] ?: @"", @"method": request[@"method"] ?: @"", @"success": @(!error), @"cancelled": @(cancelled)} mutableCopy]; if (data) response[@"data"] = data; if (error) response[@"error"] = @{@"code": @"FACEBOOK_IOS_ERROR", @"message": error}; NSData *json = [NSJSONSerialization dataWithJSONObject:response options:0 error:nil]; NSString *payload = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]; auto app = CC_CURRENT_APPLICATION(); if (!app || !app->getEngine() || !payload) return; app->getEngine()->getScheduler()->performFunctionInCocosThread([payload]() { [[JsbBridgeWrapper sharedInstance] dispatchEventToScript:@"facebook.response" arg:payload]; }); } - (UIViewController *)presenter { UIViewController *controller = UIApplication.sharedApplication.delegate.window.rootViewController; while (controller.presentedViewController) controller = controller.presentedViewController; return controller; } - (void)handlePayload:(NSString *)payload { id decoded = [NSJSONSerialization JSONObjectWithData:[payload dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; if (![decoded isKindOfClass:NSDictionary.class]) return; NSDictionary *request = decoded; if (![request[@"requestId"] isKindOfClass:NSString.class] || ![request[@"method"] isKindOfClass:NSString.class]) return; NSString *method = request[@"method"]; NSDictionary *params = [request[@"params"] isKindOfClass:NSDictionary.class] ? request[@"params"] : @{}; @try { if (!self.loginManager) { [self reply:request data:nil cancelled:NO error:self.startupError ?: @"Facebook SDK 未初始化。"]; } else if ([method isEqualToString:@"init"]) { [self reply:request data:@{} cancelled:NO error:nil]; } else if ([method isEqualToString:@"login"]) { [self login:request]; } else if ([method isEqualToString:@"logout"]) { if (self.loginPending) { [self reply:request data:nil cancelled:NO error:@"Facebook 登录仍在进行中。"]; return; } [self.loginManager logOut]; [self reply:request data:@{} cancelled:NO error:nil]; } else if ([method isEqualToString:@"share"]) { [self share:request params:params]; } else if ([method isEqualToString:@"logEvent"] || [method isEqualToString:@"logPurchase"] || [method isEqualToString:@"flushEvents"]) { [self event:request params:params]; } else { [self reply:request data:nil cancelled:NO error:@"不支持的 Facebook 方法。"]; } } @catch (NSException *exception) { if ([method isEqualToString:@"login"]) self.loginPending = NO; if ([method isEqualToString:@"share"]) { self.shareRequest = nil; self.shareDialog = nil; } [self reply:request data:nil cancelled:NO error:exception.reason ?: @"Facebook SDK 调用失败。"]; } } - (void)login:(NSDictionary *)request { if (self.loginPending || self.shareRequest || !self.presenter) { [self reply:request data:nil cancelled:NO error:@"已有 Facebook 操作进行中,或没有可用窗口。"]; return; } if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) { [self reply:request data:nil cancelled:NO error:@"请返回游戏前台后重试 Facebook 登录。"]; return; } BOOL hasFacebookApp = [UIApplication.sharedApplication canOpenURL:[NSURL URLWithString:@"fbapi://"]] && [UIApplication.sharedApplication canOpenURL:[NSURL URLWithString:@"fbauth2://"]]; NSArray *permissions = @[@"public_profile", @"email"]; // Match Unity FBUnityInterface: create a manager for this login and let the // SDK handle the standard flow. Retain it through the asynchronous callback. self.loginManager = [[FBSDKLoginManager alloc] init]; self.loginPending = YES; void (^loginHandler)(FBSDKLoginManagerLoginResult *, NSError *) = ^(FBSDKLoginManagerLoginResult *result, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ self.loginPending = NO; if (error || result.isCancelled) { [self reply:request data:nil cancelled:!error && result.isCancelled error:error.localizedDescription]; return; } NSString *accessToken = result.token.tokenString; NSString *authenticationToken = result.authenticationToken.tokenString; FBSDKProfile *profile = FBSDKProfile.currentProfile; NSString *userID = result.token.userID.length ? result.token.userID : profile.userID; // Do not attach a cached profile's name to a different token's user. NSString *name = [profile.userID isEqualToString:userID] ? profile.name : nil; NSDictionary *userInfo = @{@"userId": userID ?: @"", @"name": name ?: @""}; if (accessToken.length) { NSMutableDictionary *data = [userInfo mutableCopy]; data[@"accessToken"] = accessToken; data[@"tokenType"] = @"access"; [self reply:request data:data cancelled:NO error:nil]; } else if (authenticationToken.length) { // The standard entry point generates its own nonce. Return the // SDK token's nonce, never an unrelated locally generated value. NSString *nonce = result.authenticationToken.nonce; if (!nonce.length) { [self reply:request data:nil cancelled:NO error:@"Facebook 身份令牌缺少 nonce。"]; return; } NSMutableDictionary *data = [userInfo mutableCopy]; data[@"authenticationToken"] = authenticationToken; data[@"tokenType"] = @"authentication"; data[@"nonce"] = nonce; [self reply:request data:data cancelled:NO error:nil]; } else { [self reply:request data:nil cancelled:NO error:@"Facebook 登录未返回有效令牌。"]; } }); }; if (hasFacebookApp) { // Same entry point as Unity startLogin:scope:isPublishPermLogin:. // nil asks the SDK to find its presenting view controller automatically. [self.loginManager logInWithPermissions:permissions fromViewController:nil handler:loginHandler]; } else { // Same configuration-based entry point as Unity loginWithTrackingPreference:. FBSDKLoginConfiguration *config = [[FBSDKLoginConfiguration alloc] initWithPermissions:permissions tracking:FBSDKLoginTrackingLimited nonce:NSUUID.UUID.UUIDString appSwitch:FBSDKAppSwitchDisabled]; if (!config) { self.loginPending = NO; [self reply:request data:nil cancelled:NO error:@"Facebook 登录配置创建失败。"]; return; } [self.loginManager logInFromViewController:nil configuration:config completion:loginHandler]; } } - (void)share:(NSDictionary *)request params:(NSDictionary *)params { if (self.shareRequest || self.loginPending || !self.presenter) { [self reply:request data:nil cancelled:NO error:@"已有 Facebook 操作进行中,或没有可用窗口。"]; return; } NSString *value = [params[@"url"] isKindOfClass:NSString.class] ? params[@"url"] : @""; NSURL *url = [NSURL URLWithString:value]; if (!url.host.length || ![@[@"https", @"http"] containsObject:url.scheme.lowercaseString]) { [self reply:request data:nil cancelled:NO error:@"分享链接必须为有效的 HTTP(S) URL。"]; return; } FBSDKShareLinkContent *content = [FBSDKShareLinkContent new]; content.contentURL = url; if ([params[@"quote"] isKindOfClass:NSString.class]) content.quote = params[@"quote"]; if ([params[@"hashtag"] isKindOfClass:NSString.class] && [params[@"hashtag"] length]) { content.hashtag = [[FBSDKHashtag alloc] initWithString:params[@"hashtag"]]; } self.shareRequest = request; self.shareDialog = [[FBSDKShareDialog alloc] initWithViewController:self.presenter content:content delegate:self]; self.shareDialog.mode = FBSDKShareDialogModeAutomatic; if (![self.shareDialog canShow] || ![self.shareDialog show]) { if (self.shareRequest) [self reply:request data:nil cancelled:NO error:@"无法打开 Facebook 分享窗口。"]; self.shareRequest = nil; self.shareDialog = nil; } } - (void)sharer:(id)sharer didCompleteWithResults:(NSDictionary *)results { if (self.shareRequest) [self reply:self.shareRequest data:@{@"postId": results[@"postId"] ?: @""} cancelled:NO error:nil]; self.shareRequest = nil; self.shareDialog = nil; } - (void)sharer:(id)sharer didFailWithError:(NSError *)error { if (self.shareRequest) [self reply:self.shareRequest data:nil cancelled:NO error:error.localizedDescription]; self.shareRequest = nil; self.shareDialog = nil; } - (void)sharerDidCancel:(id)sharer { if (self.shareRequest) [self reply:self.shareRequest data:nil cancelled:YES error:nil]; self.shareRequest = nil; self.shareDialog = nil; } - (void)event:(NSDictionary *)request params:(NSDictionary *)params { NSString *method = request[@"method"]; NSDictionary *values = params[@"parameters"] ?: @{}; if (![values isKindOfClass:NSDictionary.class]) { [self reply:request data:nil cancelled:NO error:@"事件参数必须为字典。"]; return; } for (id key in values) { id value = values[key]; if (![key isKindOfClass:NSString.class] || (![value isKindOfClass:NSString.class] && ![value isKindOfClass:NSNumber.class])) { [self reply:request data:nil cancelled:NO error:@"事件参数只支持字符串或数字。"]; return; } } if ([method isEqualToString:@"flushEvents"]) { [FBSDKAppEvents.shared flush]; } else if ([method isEqualToString:@"logPurchase"]) { NSNumber *amount = params[@"amount"]; NSString *currency = params[@"currency"]; if (![amount isKindOfClass:NSNumber.class] || !std::isfinite(amount.doubleValue) || amount.doubleValue < 0 || ![currency isKindOfClass:NSString.class] || [currency rangeOfString:@"^[A-Za-z]{3}$" options:NSRegularExpressionSearch].location == NSNotFound) { [self reply:request data:nil cancelled:NO error:@"购买金额或 ISO 4217 货币代码无效。"]; return; } [FBSDKAppEvents.shared logPurchase:amount.doubleValue currency:currency.uppercaseString parameters:values]; } else { NSString *name = params[@"eventName"]; NSNumber *sum = params[@"valueToSum"]; if (![name isKindOfClass:NSString.class] || !name.length || (sum && (![sum isKindOfClass:NSNumber.class] || !std::isfinite(sum.doubleValue)))) { [self reply:request data:nil cancelled:NO error:@"事件名称或累计数值无效。"]; return; } if (sum) [FBSDKAppEvents.shared logEvent:name valueToSum:sum.doubleValue parameters:values]; else [FBSDKAppEvents.shared logEvent:name parameters:values]; } // Accepted by the local SDK, not a delivery receipt from Meta. [self reply:request data:@{@"accepted": @YES} cancelled:NO error:nil]; } @end