|
@@ -0,0 +1,209 @@
|
|
|
|
|
+#import "FBWrapper.h"
|
|
|
|
|
+#import <FBSDKCoreKit/FBSDKCoreKit.h>
|
|
|
|
|
+#import <FBSDKLoginKit/FBSDKLoginKit.h>
|
|
|
|
|
+#import <FBSDKShareKit/FBSDKShareKit.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.
|
|
|
|
|
+ [[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 = @"请在 SDK 管理构建面板启用 Facebook 并填写 App ID 和 Client Token。";
|
|
|
|
|
+ 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<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
|