FBWrapper.mm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. #import "FBWrapper.h"
  2. #import <FBSDKCoreKit/FBSDKCoreKit.h>
  3. #import <FBSDKCoreKit/FBSDKCoreKit-Swift.h>
  4. #import <FBSDKLoginKit/FBSDKLoginKit.h>
  5. #import <FBSDKLoginKit/FBSDKLoginKit-Swift.h>
  6. #import <FBSDKShareKit/FBSDKShareKit.h>
  7. #import <FBSDKShareKit/FBSDKShareKit-Swift.h>
  8. #import "platform/apple/JsbBridgeWrapper.h"
  9. #include "application/ApplicationManager.h"
  10. #include "application/BaseApplication.h"
  11. #include "base/Scheduler.h"
  12. #include "engine/BaseEngine.h"
  13. #include <cmath>
  14. @interface FBWrapper () <FBSDKSharingDelegate>
  15. @property(nonatomic, strong) FBSDKLoginManager *loginManager;
  16. @property(nonatomic, strong) FBSDKShareDialog *shareDialog;
  17. @property(nonatomic, strong) NSDictionary *shareRequest;
  18. @property(nonatomic, copy) NSString *startupError;
  19. @property(nonatomic) BOOL loginPending;
  20. @end
  21. @implementation FBWrapper
  22. + (instancetype)shared {
  23. static FBWrapper *instance;
  24. static dispatch_once_t once;
  25. dispatch_once(&once, ^{ instance = [FBWrapper new]; });
  26. return instance;
  27. }
  28. - (void)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)options {
  29. // Register even on configuration failure so JS receives a useful error instead of timing out.
  30. [[JsbBridgeWrapper sharedInstance] addScriptEventListener:@"facebook.request" listener:^(NSString *payload) {
  31. dispatch_async(dispatch_get_main_queue(), ^{ [self handlePayload:payload]; });
  32. }];
  33. NSDictionary *info = NSBundle.mainBundle.infoDictionary;
  34. if (![info[@"FacebookAppID"] isKindOfClass:NSString.class] || ![info[@"FacebookAppID"] length] ||
  35. ![info[@"FacebookClientToken"] isKindOfClass:NSString.class] || ![info[@"FacebookClientToken"] length]) {
  36. self.startupError = @"请在 native/engine/ios/Info.plist 中填写 FacebookAppID 和 FacebookClientToken。";
  37. return;
  38. }
  39. @try {
  40. [[FBSDKApplicationDelegate sharedInstance] application:application didFinishLaunchingWithOptions:options];
  41. self.loginManager = [FBSDKLoginManager new];
  42. } @catch (NSException *exception) {
  43. self.startupError = exception.reason ?: @"Facebook 初始化失败。";
  44. }
  45. }
  46. - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options {
  47. if (!self.loginManager) return NO;
  48. return [[FBSDKApplicationDelegate sharedInstance] application:application openURL:url options:options];
  49. }
  50. - (void)reply:(NSDictionary *)request data:(NSDictionary *)data cancelled:(BOOL)cancelled error:(NSString *)error {
  51. NSMutableDictionary *response = [@{@"requestId": request[@"requestId"] ?: @"",
  52. @"method": request[@"method"] ?: @"", @"success": @(!error), @"cancelled": @(cancelled)} mutableCopy];
  53. if (data) response[@"data"] = data;
  54. if (error) response[@"error"] = @{@"code": @"FACEBOOK_IOS_ERROR", @"message": error};
  55. NSData *json = [NSJSONSerialization dataWithJSONObject:response options:0 error:nil];
  56. NSString *payload = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding];
  57. auto app = CC_CURRENT_APPLICATION();
  58. if (!app || !app->getEngine() || !payload) return;
  59. app->getEngine()->getScheduler()->performFunctionInCocosThread([payload]() {
  60. [[JsbBridgeWrapper sharedInstance] dispatchEventToScript:@"facebook.response" arg:payload];
  61. });
  62. }
  63. - (UIViewController *)presenter {
  64. UIViewController *controller = UIApplication.sharedApplication.delegate.window.rootViewController;
  65. while (controller.presentedViewController) controller = controller.presentedViewController;
  66. return controller;
  67. }
  68. - (void)handlePayload:(NSString *)payload {
  69. id decoded = [NSJSONSerialization JSONObjectWithData:[payload dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
  70. if (![decoded isKindOfClass:NSDictionary.class]) return;
  71. NSDictionary *request = decoded;
  72. if (![request[@"requestId"] isKindOfClass:NSString.class] || ![request[@"method"] isKindOfClass:NSString.class]) return;
  73. NSString *method = request[@"method"];
  74. NSDictionary *params = [request[@"params"] isKindOfClass:NSDictionary.class] ? request[@"params"] : @{};
  75. @try {
  76. if (!self.loginManager) {
  77. [self reply:request data:nil cancelled:NO error:self.startupError ?: @"Facebook SDK 未初始化。"];
  78. } else if ([method isEqualToString:@"init"]) {
  79. [self reply:request data:@{} cancelled:NO error:nil];
  80. } else if ([method isEqualToString:@"login"]) {
  81. [self login:request];
  82. } else if ([method isEqualToString:@"logout"]) {
  83. if (self.loginPending) {
  84. [self reply:request data:nil cancelled:NO error:@"Facebook 登录仍在进行中。"];
  85. return;
  86. }
  87. [self.loginManager logOut];
  88. [self reply:request data:@{} cancelled:NO error:nil];
  89. } else if ([method isEqualToString:@"share"]) {
  90. [self share:request params:params];
  91. } else if ([method isEqualToString:@"logEvent"] || [method isEqualToString:@"logPurchase"] || [method isEqualToString:@"flushEvents"]) {
  92. [self event:request params:params];
  93. } else {
  94. [self reply:request data:nil cancelled:NO error:@"不支持的 Facebook 方法。"];
  95. }
  96. } @catch (NSException *exception) {
  97. if ([method isEqualToString:@"login"]) self.loginPending = NO;
  98. if ([method isEqualToString:@"share"]) { self.shareRequest = nil; self.shareDialog = nil; }
  99. [self reply:request data:nil cancelled:NO error:exception.reason ?: @"Facebook SDK 调用失败。"];
  100. }
  101. }
  102. - (void)login:(NSDictionary *)request {
  103. if (self.loginPending || self.shareRequest || !self.presenter) {
  104. [self reply:request data:nil cancelled:NO error:@"已有 Facebook 操作进行中,或没有可用窗口。"];
  105. return;
  106. }
  107. // Limited Login works without an ATT prompt. Each attempt gets its own nonce.
  108. NSString *nonce = NSUUID.UUID.UUIDString;
  109. FBSDKLoginConfiguration *config = [[FBSDKLoginConfiguration alloc]
  110. initWithPermissions:@[@"public_profile", @"email"] tracking:FBSDKLoginTrackingLimited nonce:nonce];
  111. self.loginPending = YES;
  112. [self.loginManager logInFromViewController:self.presenter configuration:config
  113. completion:^(FBSDKLoginManagerLoginResult *result, NSError *error) {
  114. self.loginPending = NO;
  115. if (error) { [self reply:request data:nil cancelled:NO error:error.localizedDescription]; return; }
  116. if (result.isCancelled) { [self reply:request data:nil cancelled:YES error:nil]; return; }
  117. NSString *token = result.authenticationToken.tokenString;
  118. if (!token.length) { [self reply:request data:nil cancelled:NO error:@"Facebook 未返回身份令牌。"] ; return; }
  119. [self reply:request data:@{@"authenticationToken": token, @"nonce": nonce} cancelled:NO error:nil];
  120. }];
  121. }
  122. - (void)share:(NSDictionary *)request params:(NSDictionary *)params {
  123. if (self.shareRequest || self.loginPending || !self.presenter) {
  124. [self reply:request data:nil cancelled:NO error:@"已有 Facebook 操作进行中,或没有可用窗口。"];
  125. return;
  126. }
  127. NSString *value = [params[@"url"] isKindOfClass:NSString.class] ? params[@"url"] : @"";
  128. NSURL *url = [NSURL URLWithString:value];
  129. if (!url.host.length || ![@[@"https", @"http"] containsObject:url.scheme.lowercaseString]) {
  130. [self reply:request data:nil cancelled:NO error:@"分享链接必须为有效的 HTTP(S) URL。"];
  131. return;
  132. }
  133. FBSDKShareLinkContent *content = [FBSDKShareLinkContent new];
  134. content.contentURL = url;
  135. if ([params[@"quote"] isKindOfClass:NSString.class]) content.quote = params[@"quote"];
  136. if ([params[@"hashtag"] isKindOfClass:NSString.class] && [params[@"hashtag"] length]) {
  137. content.hashtag = [[FBSDKHashtag alloc] initWithString:params[@"hashtag"]];
  138. }
  139. self.shareRequest = request;
  140. self.shareDialog = [[FBSDKShareDialog alloc] initWithViewController:self.presenter content:content delegate:self];
  141. self.shareDialog.mode = FBSDKShareDialogModeAutomatic;
  142. if (![self.shareDialog canShow] || ![self.shareDialog show]) {
  143. if (self.shareRequest) [self reply:request data:nil cancelled:NO error:@"无法打开 Facebook 分享窗口。"];
  144. self.shareRequest = nil;
  145. self.shareDialog = nil;
  146. }
  147. }
  148. - (void)sharer:(id<FBSDKSharing>)sharer didCompleteWithResults:(NSDictionary *)results {
  149. if (self.shareRequest) [self reply:self.shareRequest data:@{@"postId": results[@"postId"] ?: @""} cancelled:NO error:nil];
  150. self.shareRequest = nil; self.shareDialog = nil;
  151. }
  152. - (void)sharer:(id<FBSDKSharing>)sharer didFailWithError:(NSError *)error {
  153. if (self.shareRequest) [self reply:self.shareRequest data:nil cancelled:NO error:error.localizedDescription];
  154. self.shareRequest = nil; self.shareDialog = nil;
  155. }
  156. - (void)sharerDidCancel:(id<FBSDKSharing>)sharer {
  157. if (self.shareRequest) [self reply:self.shareRequest data:nil cancelled:YES error:nil];
  158. self.shareRequest = nil; self.shareDialog = nil;
  159. }
  160. - (void)event:(NSDictionary *)request params:(NSDictionary *)params {
  161. NSString *method = request[@"method"];
  162. NSDictionary *values = params[@"parameters"] ?: @{};
  163. if (![values isKindOfClass:NSDictionary.class]) {
  164. [self reply:request data:nil cancelled:NO error:@"事件参数必须为字典。"];
  165. return;
  166. }
  167. for (id key in values) {
  168. id value = values[key];
  169. if (![key isKindOfClass:NSString.class] ||
  170. (![value isKindOfClass:NSString.class] && ![value isKindOfClass:NSNumber.class])) {
  171. [self reply:request data:nil cancelled:NO error:@"事件参数只支持字符串或数字。"];
  172. return;
  173. }
  174. }
  175. if ([method isEqualToString:@"flushEvents"]) {
  176. [FBSDKAppEvents.shared flush];
  177. } else if ([method isEqualToString:@"logPurchase"]) {
  178. NSNumber *amount = params[@"amount"];
  179. NSString *currency = params[@"currency"];
  180. if (![amount isKindOfClass:NSNumber.class] || !std::isfinite(amount.doubleValue) || amount.doubleValue < 0 ||
  181. ![currency isKindOfClass:NSString.class] ||
  182. [currency rangeOfString:@"^[A-Za-z]{3}$" options:NSRegularExpressionSearch].location == NSNotFound) {
  183. [self reply:request data:nil cancelled:NO error:@"购买金额或 ISO 4217 货币代码无效。"];
  184. return;
  185. }
  186. [FBSDKAppEvents.shared logPurchase:amount.doubleValue currency:currency.uppercaseString parameters:values];
  187. } else {
  188. NSString *name = params[@"eventName"];
  189. NSNumber *sum = params[@"valueToSum"];
  190. if (![name isKindOfClass:NSString.class] || !name.length ||
  191. (sum && (![sum isKindOfClass:NSNumber.class] || !std::isfinite(sum.doubleValue)))) {
  192. [self reply:request data:nil cancelled:NO error:@"事件名称或累计数值无效。"];
  193. return;
  194. }
  195. if (sum) [FBSDKAppEvents.shared logEvent:name valueToSum:sum.doubleValue parameters:values];
  196. else [FBSDKAppEvents.shared logEvent:name parameters:values];
  197. }
  198. // Accepted by the local SDK, not a delivery receipt from Meta.
  199. [self reply:request data:@{@"accepted": @YES} cancelled:NO error:nil];
  200. }
  201. @end