| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- #import "FBWrapper.h"
- // Swift-generated headers need these protocol definitions when compiled as Objective-C++.
- #import <SafariServices/SafariServices.h>
- #import <AuthenticationServices/AuthenticationServices.h>
- #import <FBSDKCoreKit/FBSDKCoreKit.h>
- #import <FBSDKCoreKit/FBSDKCoreKit-Swift.h>
- #import <FBSDKLoginKit/FBSDKLoginKit.h>
- #import <FBSDKLoginKit/FBSDKLoginKit-Swift.h>
- #import <FBSDKShareKit/FBSDKShareKit.h>
- #import <FBSDKShareKit/FBSDKShareKit-Swift.h>
- #import "platform/apple/JsbBridgeWrapper.h"
- #include "application/ApplicationManager.h"
- #include "application/BaseApplication.h"
- #include "base/Scheduler.h"
- #include "engine/BaseEngine.h"
- #include <cmath>
- @interface FBWrapper () <FBSDKSharingDelegate>
- @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<NSString *> *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<FBSDKSharing>)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<FBSDKSharing>)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<FBSDKSharing>)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
|