FBWrapper.mm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. #import "FBWrapper.h"
  2. // Swift-generated headers need these protocol definitions when compiled as Objective-C++.
  3. #import <SafariServices/SafariServices.h>
  4. #import <AuthenticationServices/AuthenticationServices.h>
  5. #import <AppTrackingTransparency/AppTrackingTransparency.h>
  6. #import <FBSDKCoreKit/FBSDKCoreKit.h>
  7. #import <FBSDKCoreKit/FBSDKCoreKit-Swift.h>
  8. #import <FBSDKLoginKit/FBSDKLoginKit.h>
  9. #import <FBSDKLoginKit/FBSDKLoginKit-Swift.h>
  10. #import <FBSDKShareKit/FBSDKShareKit.h>
  11. #import <FBSDKShareKit/FBSDKShareKit-Swift.h>
  12. #import "platform/apple/JsbBridgeWrapper.h"
  13. #include "application/ApplicationManager.h"
  14. #include "application/BaseApplication.h"
  15. #include "base/Scheduler.h"
  16. #include "engine/BaseEngine.h"
  17. #include <cmath>
  18. @interface FBWrapper () <FBSDKSharingDelegate>
  19. @property(nonatomic, strong) FBSDKLoginManager *loginManager;
  20. @property(nonatomic, strong) FBSDKShareDialog *shareDialog;
  21. @property(nonatomic, strong) NSDictionary *shareRequest;
  22. @property(nonatomic, copy) NSString *startupError;
  23. @property(nonatomic) BOOL loginPending;
  24. @end
  25. @implementation FBWrapper
  26. + (instancetype)shared {
  27. static FBWrapper *instance;
  28. static dispatch_once_t once;
  29. dispatch_once(&once, ^{ instance = [FBWrapper new]; });
  30. return instance;
  31. }
  32. - (void)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)options {
  33. // Register even on configuration failure so JS receives a useful error instead of timing out.
  34. // Creator 3.8.8's MRC bridge releases the listener after adding it to its array.
  35. // Keep this outer block capture-free (no self or local variables): a global block
  36. // has static lifetime and is safe with that ownership behavior. GCD owns the
  37. // inner block and retains its payload until the main-queue callback runs.
  38. static OnScriptEventListener const requestListener = ^(NSString *payload) {
  39. dispatch_async(dispatch_get_main_queue(), ^{
  40. [[FBWrapper shared] handlePayload:payload];
  41. });
  42. };
  43. [[JsbBridgeWrapper sharedInstance] addScriptEventListener:@"facebook.request" listener:requestListener];
  44. NSDictionary *info = NSBundle.mainBundle.infoDictionary;
  45. if (![info[@"FacebookAppID"] isKindOfClass:NSString.class] || ![info[@"FacebookAppID"] length] ||
  46. ![info[@"FacebookClientToken"] isKindOfClass:NSString.class] || ![info[@"FacebookClientToken"] length]) {
  47. self.startupError = @"请在 native/engine/ios/Info.plist 中填写 FacebookAppID 和 FacebookClientToken。";
  48. return;
  49. }
  50. @try {
  51. [[FBSDKApplicationDelegate sharedInstance] application:application didFinishLaunchingWithOptions:options];
  52. self.loginManager = [FBSDKLoginManager new];
  53. } @catch (NSException *exception) {
  54. self.startupError = exception.reason ?: @"Facebook 初始化失败。";
  55. }
  56. }
  57. - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options {
  58. if (!self.loginManager) return NO;
  59. return [[FBSDKApplicationDelegate sharedInstance] application:application openURL:url options:options];
  60. }
  61. - (void)reply:(NSDictionary *)request data:(NSDictionary *)data cancelled:(BOOL)cancelled error:(NSString *)error {
  62. NSMutableDictionary *response = [@{@"requestId": request[@"requestId"] ?: @"",
  63. @"method": request[@"method"] ?: @"", @"success": @(!error), @"cancelled": @(cancelled)} mutableCopy];
  64. if (data) response[@"data"] = data;
  65. if (error) response[@"error"] = @{@"code": @"FACEBOOK_IOS_ERROR", @"message": error};
  66. NSData *json = [NSJSONSerialization dataWithJSONObject:response options:0 error:nil];
  67. NSString *payload = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding];
  68. auto app = CC_CURRENT_APPLICATION();
  69. if (!app || !app->getEngine() || !payload) return;
  70. app->getEngine()->getScheduler()->performFunctionInCocosThread([payload]() {
  71. [[JsbBridgeWrapper sharedInstance] dispatchEventToScript:@"facebook.response" arg:payload];
  72. });
  73. }
  74. - (UIViewController *)presenter {
  75. UIViewController *controller = UIApplication.sharedApplication.delegate.window.rootViewController;
  76. while (controller.presentedViewController) controller = controller.presentedViewController;
  77. return controller;
  78. }
  79. - (void)handlePayload:(NSString *)payload {
  80. id decoded = [NSJSONSerialization JSONObjectWithData:[payload dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
  81. if (![decoded isKindOfClass:NSDictionary.class]) return;
  82. NSDictionary *request = decoded;
  83. if (![request[@"requestId"] isKindOfClass:NSString.class] || ![request[@"method"] isKindOfClass:NSString.class]) return;
  84. NSString *method = request[@"method"];
  85. NSDictionary *params = [request[@"params"] isKindOfClass:NSDictionary.class] ? request[@"params"] : @{};
  86. @try {
  87. if (!self.loginManager) {
  88. [self reply:request data:nil cancelled:NO error:self.startupError ?: @"Facebook SDK 未初始化。"];
  89. } else if ([method isEqualToString:@"init"]) {
  90. [self reply:request data:@{} cancelled:NO error:nil];
  91. } else if ([method isEqualToString:@"login"]) {
  92. [self login:request];
  93. } else if ([method isEqualToString:@"logout"]) {
  94. if (self.loginPending) {
  95. [self reply:request data:nil cancelled:NO error:@"Facebook 登录仍在进行中。"];
  96. return;
  97. }
  98. [self.loginManager logOut];
  99. [self reply:request data:@{} cancelled:NO error:nil];
  100. } else if ([method isEqualToString:@"share"]) {
  101. [self share:request params:params];
  102. } else if ([method isEqualToString:@"logEvent"] || [method isEqualToString:@"logPurchase"] || [method isEqualToString:@"flushEvents"]) {
  103. [self event:request params:params];
  104. } else {
  105. [self reply:request data:nil cancelled:NO error:@"不支持的 Facebook 方法。"];
  106. }
  107. } @catch (NSException *exception) {
  108. if ([method isEqualToString:@"login"]) self.loginPending = NO;
  109. if ([method isEqualToString:@"share"]) { self.shareRequest = nil; self.shareDialog = nil; }
  110. [self reply:request data:nil cancelled:NO error:exception.reason ?: @"Facebook SDK 调用失败。"];
  111. }
  112. }
  113. - (void)login:(NSDictionary *)request {
  114. if (self.loginPending || self.shareRequest || !self.presenter) {
  115. [self reply:request data:nil cancelled:NO error:@"已有 Facebook 操作进行中,或没有可用窗口。"];
  116. return;
  117. }
  118. self.loginPending = YES;
  119. if (@available(iOS 14.0, *)) {
  120. if (ATTrackingManager.trackingAuthorizationStatus == ATTrackingManagerAuthorizationStatusNotDetermined) {
  121. if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
  122. self.loginPending = NO;
  123. [self reply:request data:nil cancelled:NO error:@"请返回游戏前台后重试 Facebook 登录。"];
  124. return;
  125. }
  126. [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
  127. dispatch_async(dispatch_get_main_queue(), ^{ [self prepareGraphLogin:request retries:20]; });
  128. }];
  129. return;
  130. }
  131. }
  132. [self prepareGraphLogin:request retries:20];
  133. }
  134. - (void)prepareGraphLogin:(NSDictionary *)request retries:(NSInteger)retries {
  135. // ATT completion may arrive before the app becomes active again.
  136. if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive && retries > 0) {
  137. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  138. [self prepareGraphLogin:request retries:retries - 1];
  139. });
  140. return;
  141. }
  142. if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive || !self.presenter) {
  143. self.loginPending = NO;
  144. [self reply:request data:nil cancelled:NO error:@"请返回游戏前台后重试 Facebook 登录。"];
  145. return;
  146. }
  147. if (@available(iOS 14.0, *)) {
  148. BOOL authorized = ATTrackingManager.trackingAuthorizationStatus == ATTrackingManagerAuthorizationStatusAuthorized;
  149. if (@available(iOS 17.0, *)) {
  150. // SDK 17+ reads the actual ATT status itself on iOS 17+.
  151. } else {
  152. [FBSDKSettings sharedSettings].isAdvertiserTrackingEnabled = authorized;
  153. }
  154. if (!authorized) {
  155. self.loginPending = NO;
  156. [self reply:request data:nil cancelled:NO error:@"当前隐私授权状态无法使用此 Facebook Graph API 登录方式,请使用其他登录方式。"];
  157. return;
  158. }
  159. }
  160. @try {
  161. [self startGraphLogin:request];
  162. } @catch (NSException *exception) {
  163. self.loginPending = NO;
  164. [self reply:request data:nil cancelled:NO error:exception.reason ?: @"Facebook 登录失败。"];
  165. }
  166. }
  167. - (void)startGraphLogin:(NSDictionary *)request {
  168. FBSDKLoginConfiguration *config = [[FBSDKLoginConfiguration alloc]
  169. initWithPermissions:@[@"public_profile", @"email"]
  170. tracking:FBSDKLoginTrackingEnabled nonce:NSUUID.UUID.UUIDString];
  171. [self.loginManager logInFromViewController:self.presenter configuration:config
  172. completion:^(FBSDKLoginManagerLoginResult *result, NSError *error) {
  173. if (error || result.isCancelled) {
  174. self.loginPending = NO;
  175. [self reply:request data:nil cancelled:!error && result.isCancelled error:error.localizedDescription];
  176. return;
  177. }
  178. // Never substitute AuthenticationToken (Limited Login JWT) for AccessToken.
  179. NSString *token = result.token.tokenString;
  180. if (!token.length) {
  181. self.loginPending = NO;
  182. [self reply:request data:nil cancelled:NO error:@"Facebook 未返回 Graph API Access Token,请使用其他登录方式。"];
  183. return;
  184. }
  185. @try {
  186. FBSDKGraphRequest *graph = [[FBSDKGraphRequest alloc] initWithGraphPath:@"me"
  187. parameters:@{@"fields": @"id"} tokenString:token version:nil HTTPMethod:@"GET"];
  188. [graph startWithCompletion:^(id<FBSDKGraphRequestConnecting> connection, id result, NSError *graphError) {
  189. dispatch_async(dispatch_get_main_queue(), ^{
  190. self.loginPending = NO;
  191. if (graphError) {
  192. [self reply:request data:nil cancelled:NO error:[NSString stringWithFormat:@"Facebook Graph API 校验失败:%@", graphError.localizedDescription]];
  193. return;
  194. }
  195. NSString *userID = [result isKindOfClass:NSDictionary.class] ? result[@"id"] : nil;
  196. if (![userID isKindOfClass:NSString.class] || !userID.length) {
  197. [self reply:request data:nil cancelled:NO error:@"Facebook Graph API 未返回用户 ID。"];
  198. return;
  199. }
  200. // The backend must independently validate this token and its app ID.
  201. [self reply:request data:@{@"accessToken": token, @"userId": userID} cancelled:NO error:nil];
  202. });
  203. }];
  204. } @catch (NSException *exception) {
  205. self.loginPending = NO;
  206. [self reply:request data:nil cancelled:NO error:exception.reason ?: @"Facebook Graph API 调用失败。"];
  207. }
  208. }];
  209. }
  210. - (void)share:(NSDictionary *)request params:(NSDictionary *)params {
  211. if (self.shareRequest || self.loginPending || !self.presenter) {
  212. [self reply:request data:nil cancelled:NO error:@"已有 Facebook 操作进行中,或没有可用窗口。"];
  213. return;
  214. }
  215. NSString *value = [params[@"url"] isKindOfClass:NSString.class] ? params[@"url"] : @"";
  216. NSURL *url = [NSURL URLWithString:value];
  217. if (!url.host.length || ![@[@"https", @"http"] containsObject:url.scheme.lowercaseString]) {
  218. [self reply:request data:nil cancelled:NO error:@"分享链接必须为有效的 HTTP(S) URL。"];
  219. return;
  220. }
  221. FBSDKShareLinkContent *content = [FBSDKShareLinkContent new];
  222. content.contentURL = url;
  223. if ([params[@"quote"] isKindOfClass:NSString.class]) content.quote = params[@"quote"];
  224. if ([params[@"hashtag"] isKindOfClass:NSString.class] && [params[@"hashtag"] length]) {
  225. content.hashtag = [[FBSDKHashtag alloc] initWithString:params[@"hashtag"]];
  226. }
  227. self.shareRequest = request;
  228. self.shareDialog = [[FBSDKShareDialog alloc] initWithViewController:self.presenter content:content delegate:self];
  229. self.shareDialog.mode = FBSDKShareDialogModeAutomatic;
  230. if (![self.shareDialog canShow] || ![self.shareDialog show]) {
  231. if (self.shareRequest) [self reply:request data:nil cancelled:NO error:@"无法打开 Facebook 分享窗口。"];
  232. self.shareRequest = nil;
  233. self.shareDialog = nil;
  234. }
  235. }
  236. - (void)sharer:(id<FBSDKSharing>)sharer didCompleteWithResults:(NSDictionary *)results {
  237. if (self.shareRequest) [self reply:self.shareRequest data:@{@"postId": results[@"postId"] ?: @""} cancelled:NO error:nil];
  238. self.shareRequest = nil; self.shareDialog = nil;
  239. }
  240. - (void)sharer:(id<FBSDKSharing>)sharer didFailWithError:(NSError *)error {
  241. if (self.shareRequest) [self reply:self.shareRequest data:nil cancelled:NO error:error.localizedDescription];
  242. self.shareRequest = nil; self.shareDialog = nil;
  243. }
  244. - (void)sharerDidCancel:(id<FBSDKSharing>)sharer {
  245. if (self.shareRequest) [self reply:self.shareRequest data:nil cancelled:YES error:nil];
  246. self.shareRequest = nil; self.shareDialog = nil;
  247. }
  248. - (void)event:(NSDictionary *)request params:(NSDictionary *)params {
  249. NSString *method = request[@"method"];
  250. NSDictionary *values = params[@"parameters"] ?: @{};
  251. if (![values isKindOfClass:NSDictionary.class]) {
  252. [self reply:request data:nil cancelled:NO error:@"事件参数必须为字典。"];
  253. return;
  254. }
  255. for (id key in values) {
  256. id value = values[key];
  257. if (![key isKindOfClass:NSString.class] ||
  258. (![value isKindOfClass:NSString.class] && ![value isKindOfClass:NSNumber.class])) {
  259. [self reply:request data:nil cancelled:NO error:@"事件参数只支持字符串或数字。"];
  260. return;
  261. }
  262. }
  263. if ([method isEqualToString:@"flushEvents"]) {
  264. [FBSDKAppEvents.shared flush];
  265. } else if ([method isEqualToString:@"logPurchase"]) {
  266. NSNumber *amount = params[@"amount"];
  267. NSString *currency = params[@"currency"];
  268. if (![amount isKindOfClass:NSNumber.class] || !std::isfinite(amount.doubleValue) || amount.doubleValue < 0 ||
  269. ![currency isKindOfClass:NSString.class] ||
  270. [currency rangeOfString:@"^[A-Za-z]{3}$" options:NSRegularExpressionSearch].location == NSNotFound) {
  271. [self reply:request data:nil cancelled:NO error:@"购买金额或 ISO 4217 货币代码无效。"];
  272. return;
  273. }
  274. [FBSDKAppEvents.shared logPurchase:amount.doubleValue currency:currency.uppercaseString parameters:values];
  275. } else {
  276. NSString *name = params[@"eventName"];
  277. NSNumber *sum = params[@"valueToSum"];
  278. if (![name isKindOfClass:NSString.class] || !name.length ||
  279. (sum && (![sum isKindOfClass:NSNumber.class] || !std::isfinite(sum.doubleValue)))) {
  280. [self reply:request data:nil cancelled:NO error:@"事件名称或累计数值无效。"];
  281. return;
  282. }
  283. if (sum) [FBSDKAppEvents.shared logEvent:name valueToSum:sum.doubleValue parameters:values];
  284. else [FBSDKAppEvents.shared logEvent:name parameters:values];
  285. }
  286. // Accepted by the local SDK, not a delivery receipt from Meta.
  287. [self reply:request data:@{@"accepted": @YES} cancelled:NO error:nil];
  288. }
  289. @end