#import "FBWrapper.h" #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. [[JsbBridgeWrapper sharedInstance] addScriptEventListener:@"facebook.request" listener:^(NSString *payload) { dispatch_async(dispatch_get_main_queue(), ^{ [self handlePayload:payload]; }); }]; 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; } // Limited Login works without an ATT prompt. Each attempt gets its own nonce. NSString *nonce = NSUUID.UUID.UUIDString; FBSDKLoginConfiguration *config = [[FBSDKLoginConfiguration alloc] initWithPermissions:@[@"public_profile", @"email"] tracking:FBSDKLoginTrackingLimited nonce:nonce]; self.loginPending = YES; [self.loginManager logInFromViewController:self.presenter configuration:config completion:^(FBSDKLoginManagerLoginResult *result, NSError *error) { self.loginPending = NO; if (error) { [self reply:request data:nil cancelled:NO error:error.localizedDescription]; return; } if (result.isCancelled) { [self reply:request data:nil cancelled:YES error:nil]; return; } NSString *token = result.authenticationToken.tokenString; if (!token.length) { [self reply:request data:nil cancelled:NO error:@"Facebook 未返回身份令牌。"] ; return; } [self reply:request data:@{@"authenticationToken": token, @"nonce": nonce} cancelled:NO error:nil]; }]; } - (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