baxi 6 天之前
父节点
当前提交
cd1ec154f0

+ 5 - 3
assets/scripts/services/login/LoginService.ts

@@ -16,10 +16,12 @@ class LoginService {
 
     async fbLogin() {
         const res = await AppKernel.fb.login();
+        if (res.isCancel) return false;
         if (res.accessToken) {
-            await this.login3rd('facebook', res.accessToken);
+            return await this.login3rd('facebook', res.accessToken);
         }
-        console.log("facebook login res: ", res);
+        AppKernel.ui.toast(res.reason || 'Facebook 未返回可用的 Access Token,请使用其他登录方式。');
+        return false;
     }
 
     async googleLogin(res?: IGoogleLoginRes) {
@@ -71,4 +73,4 @@ class LoginService {
 }
 
 
-export const loginService = new LoginService();
+export const loginService = new LoginService();

+ 2 - 0
native/engine/ios/CMakeLists.txt

@@ -21,6 +21,8 @@ cc_ios_after_target(${EXECUTABLE_NAME})
 
 target_sources(${EXECUTABLE_NAME} PRIVATE ${CC_PROJECT_DIR}/service/FBWrapper.mm)
 set_source_files_properties(${CC_PROJECT_DIR}/service/FBWrapper.mm PROPERTIES COMPILE_FLAGS "-fobjc-arc")
+# ATT is only called on iOS 14+, while the application still supports iOS 12.
+target_link_libraries(${EXECUTABLE_NAME} "-weak_framework AppTrackingTransparency")
 target_sources(${EXECUTABLE_NAME} PRIVATE ${CC_PROJECT_DIR}/service/CocosIOSPlatformBridge.mm)
 set_source_files_properties(${CC_PROJECT_DIR}/service/CocosIOSPlatformBridge.mm PROPERTIES COMPILE_FLAGS "-fobjc-arc")
 set_target_properties(${EXECUTABLE_NAME} PROPERTIES

+ 12 - 7
native/engine/ios/FACEBOOK.md

@@ -1,6 +1,6 @@
 # Facebook iOS 接入(Creator 3.8.x)
 
-实现初始化、Limited Login、登出、链接分享、自定义事件、购买事件和事件刷新。
+实现初始化、普通 Facebook 登录及 Graph API 校验、登出、链接分享、自定义事件、购买事件和事件刷新。
 TypeScript 的 FBIOSImpl 与 Android 共用 FBNativeImpl 请求桥;原生由 service/FBWrapper.mm 实现。
 AppDelegate 直接持有 FBWrapper 单例,无需在 service.json 再次注册。
 
@@ -8,7 +8,8 @@ AppDelegate 直接持有 FBWrapper 单例,无需在 service.json 再次注册
 
 1. 直接维护本目录的 `Info.plist`,然后在 Creator 构建并生成 Xcode 工程。sdk-manager 已弃用,不依赖构建面板配置。
    `FacebookAppID`、`FacebookClientToken` 已沿用 Android 当前配置;`FacebookAutoLogAppEventsEnabled` 为 `false`,与 Android 一致。
-   修改 App ID 时,必须同时更新 `CFBundleURLTypes` 中的 `fb<AppID>`。保留 `LSApplicationQueriesSchemes` 中的 `fbapi` 和 `fb-messenger-share-api`。
+   修改 App ID 时,必须同时更新 `CFBundleURLTypes` 中的 `fb<AppID>`。保留 `LSApplicationQueriesSchemes` 中的 `fbapi`、`fbauth2` 和 `fb-messenger-share-api`。
+   `NSUserTrackingUsageDescription` 用于首次登录时的 ATT 系统弹窗,发布前确认说明与实际数据用途一致。
 2. 安装 CocoaPods 后,在本目录执行(替换为生成工程的实际绝对路径):
 
    ```sh
@@ -42,16 +43,20 @@ FBWrapper 的注册入口使用不捕获 self 或局部变量的全局 Block,
 
 ## 登录结果与后端
 
-默认使用 Limited Login,不主动请求 ATT,不开启广告追踪。
-成功时 `status === 'connected'`,返回 `authenticationToken`(JWT)、`nonce`、`tokenType: 'authentication'`;`accessToken` 为空。
-服务端需要验证 Meta JWT 的签名、issuer、audience(App ID)、有效期和 nonce,并做防重放处理。
-不能把 JWT 直接交给现有只接受 Graph API Access Token 的接口;业务登录接口需支持此类令牌后才能完成服务器登录。
-当前没有修改业务登录接口或 TrackEvents.ts。
+当前使用 `FBSDKLoginTrackingEnabled`。iOS 14+ 在 ATT 状态未确定时请求一次系统授权;拒绝或受限时明确返回错误,用户可使用现有游客或账号等其他登录方式。
+iOS 14–16 将实际 ATT 结果同步给 SDK;iOS 17+ 由 SDK 自行读取系统授权状态,不伪造授权。
+授权成功后读取本次登录结果的 Access Token,并调用 Graph API `GET /me?fields=id`,只有返回有效用户 ID 才报告登录成功。
+SDK 未返回 Access Token(包括回退为 Limited Login 的情况)或 Graph API 请求失败时返回错误,不把 JWT 当成 Access Token。
+TypeScript 成功结果的 `accessToken` 交给现有 `login3rd('facebook', token)`,服务端仍须独立验证令牌有效性、所属 App ID 和用户身份,客户端 `/me` 校验不能替代服务端校验。
+LoginService 对失败显示原因、取消静默返回,不再输出完整登录令牌。业务请求协议和 TrackEvents.ts 不变。
+Graph API 本身不是登录页面:登录授权仍由 Facebook SDK 完成,Graph API 用于授权后的令牌/用户信息验证。
 
 ## 验证清单
 
 - 初始化失败能返回配置错误;正常初始化后 isInitialized 为 true。
 - 真机登录成功/取消/失败;浏览器或 Facebook App 返回游戏;登出后可再次登录。
+- 首次 ATT 允许/拒绝、已拒绝和系统限制场景;允许后 Graph API 成功并继续服务器登录,其他情况可使用其他登录方式。
+- Graph API 网络失败或令牌不可用时应显示错误,并可重试;日志中不得包含完整令牌。
 - 链接分享成功/取消/失败;重复操作被拒绝。
 - Meta 测试事件里检查 logEvent、logPurchase 和 flushEvents;返回 true 仅代表本地 SDK 接受调用。
 - 自动应用事件由 FacebookAutoLogAppEventsEnabled 控制;手动事件仍由业务显式调用。

+ 2 - 0
native/engine/ios/Info.plist

@@ -8,6 +8,8 @@
 	<string>b3c949e52128cc10879901661efb2ca5</string>
 	<key>FacebookAutoLogAppEventsEnabled</key>
 	<false/>
+	<key>NSUserTrackingUsageDescription</key>
+	<string>允许 Meta 将本应用中的活动与其他公司应用和网站中的活动关联,用于个性化广告和广告效果衡量。</string>
 	<key>CFBundleURLTypes</key>
 	<array>
 		<dict>

+ 90 - 10
native/engine/ios/service/FBWrapper.mm

@@ -2,6 +2,7 @@
 // Swift-generated headers need these protocol definitions when compiled as Objective-C++.
 #import <SafariServices/SafariServices.h>
 #import <AuthenticationServices/AuthenticationServices.h>
+#import <AppTrackingTransparency/AppTrackingTransparency.h>
 #import <FBSDKCoreKit/FBSDKCoreKit.h>
 #import <FBSDKCoreKit/FBSDKCoreKit-Swift.h>
 #import <FBSDKLoginKit/FBSDKLoginKit.h>
@@ -122,19 +123,98 @@
         [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;
+    if (@available(iOS 14.0, *)) {
+        if (ATTrackingManager.trackingAuthorizationStatus == ATTrackingManagerAuthorizationStatusNotDetermined) {
+            if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
+                self.loginPending = NO;
+                [self reply:request data:nil cancelled:NO error:@"请返回游戏前台后重试 Facebook 登录。"];
+                return;
+            }
+            [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
+                dispatch_async(dispatch_get_main_queue(), ^{ [self prepareGraphLogin:request retries:20]; });
+            }];
+            return;
+        }
+    }
+    [self prepareGraphLogin:request retries:20];
+}
+
+- (void)prepareGraphLogin:(NSDictionary *)request retries:(NSInteger)retries {
+    // ATT completion may arrive before the app becomes active again.
+    if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive && retries > 0) {
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            [self prepareGraphLogin:request retries:retries - 1];
+        });
+        return;
+    }
+    if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive || !self.presenter) {
+        self.loginPending = NO;
+        [self reply:request data:nil cancelled:NO error:@"请返回游戏前台后重试 Facebook 登录。"];
+        return;
+    }
+    if (@available(iOS 14.0, *)) {
+        BOOL authorized = ATTrackingManager.trackingAuthorizationStatus == ATTrackingManagerAuthorizationStatusAuthorized;
+        if (@available(iOS 17.0, *)) {
+            // SDK 17+ reads the actual ATT status itself on iOS 17+.
+        } else {
+            [FBSDKSettings sharedSettings].isAdvertiserTrackingEnabled = authorized;
+        }
+        if (!authorized) {
+            self.loginPending = NO;
+            [self reply:request data:nil cancelled:NO error:@"当前隐私授权状态无法使用此 Facebook Graph API 登录方式,请使用其他登录方式。"];
+            return;
+        }
+    }
+    @try {
+        [self startGraphLogin:request];
+    } @catch (NSException *exception) {
+        self.loginPending = NO;
+        [self reply:request data:nil cancelled:NO error:exception.reason ?: @"Facebook 登录失败。"];
+    }
+}
+
+- (void)startGraphLogin:(NSDictionary *)request {
+    FBSDKLoginConfiguration *config = [[FBSDKLoginConfiguration alloc]
+        initWithPermissions:@[@"public_profile", @"email"]
+        tracking:FBSDKLoginTrackingEnabled nonce:NSUUID.UUID.UUIDString];
     [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];
+        if (error || result.isCancelled) {
+            self.loginPending = NO;
+            [self reply:request data:nil cancelled:!error && result.isCancelled error:error.localizedDescription];
+            return;
+        }
+        // Never substitute AuthenticationToken (Limited Login JWT) for AccessToken.
+        NSString *token = result.token.tokenString;
+        if (!token.length) {
+            self.loginPending = NO;
+            [self reply:request data:nil cancelled:NO error:@"Facebook 未返回 Graph API Access Token,请使用其他登录方式。"];
+            return;
+        }
+        @try {
+            FBSDKGraphRequest *graph = [[FBSDKGraphRequest alloc] initWithGraphPath:@"me"
+                parameters:@{@"fields": @"id"} tokenString:token version:nil HTTPMethod:@"GET"];
+            [graph startWithCompletion:^(id<FBSDKGraphRequestConnecting> connection, id result, NSError *graphError) {
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    self.loginPending = NO;
+                    if (graphError) {
+                        [self reply:request data:nil cancelled:NO error:[NSString stringWithFormat:@"Facebook Graph API 校验失败:%@", graphError.localizedDescription]];
+                        return;
+                    }
+                    NSString *userID = [result isKindOfClass:NSDictionary.class] ? result[@"id"] : nil;
+                    if (![userID isKindOfClass:NSString.class] || !userID.length) {
+                        [self reply:request data:nil cancelled:NO error:@"Facebook Graph API 未返回用户 ID。"];
+                        return;
+                    }
+                    // The backend must independently validate this token and its app ID.
+                    [self reply:request data:@{@"accessToken": token, @"userId": userID} cancelled:NO error:nil];
+                });
+            }];
+        } @catch (NSException *exception) {
+            self.loginPending = NO;
+            [self reply:request data:nil cancelled:NO error:exception.reason ?: @"Facebook Graph API 调用失败。"];
+        }
     }];
 }