baxi 6 zile în urmă
părinte
comite
50f2f63de6

+ 1 - 1
assets/scripts/platform/FaceBook/FBNativeImpl.ts

@@ -100,7 +100,7 @@ export class FBNativeImpl extends FacebookImpl {
                     authenticationToken,
                     nonce: response.data?.nonce,
                     tokenType: "authentication" as const,
-                } : {}),
+                } : { tokenType: "access" as const }),
                 isCancel: false,
                 status: "connected",
             };

+ 6 - 0
assets/scripts/services/login/LoginService.ts

@@ -20,6 +20,12 @@ class LoginService {
         if (res.accessToken) {
             return await this.login3rd('facebook', res.accessToken);
         }
+        if (res.authenticationToken) {
+            // The current backend accepts Access Tokens only. Do not send an OIDC JWT
+            // through that endpoint or report a game login success without a session.
+            AppKernel.ui.toast('Facebook 授权已完成,但服务器尚未支持此 iOS 登录方式,请暂时使用其他登录方式。');
+            return false;
+        }
         AppKernel.ui.toast(res.reason || 'Facebook 未返回可用的 Access Token,请使用其他登录方式。');
         return false;
     }

+ 1 - 1
native/engine/ios/CMakeLists.txt

@@ -21,7 +21,7 @@ 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.
+# Read existing ATT status on iOS 14+ without prompting; still support 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")

+ 20 - 11
native/engine/ios/FACEBOOK.md

@@ -1,6 +1,6 @@
 # Facebook iOS 接入(Creator 3.8.x)
 
-实现初始化、普通 Facebook 登录及 Graph API 校验、登出、链接分享、自定义事件、购买事件和事件刷新。
+实现初始化、优先 Facebook App 登录及 Limited Login 网页回退、登出、链接分享、自定义事件、购买事件和事件刷新。
 TypeScript 的 FBIOSImpl 与 Android 共用 FBNativeImpl 请求桥;原生由 service/FBWrapper.mm 实现。
 AppDelegate 直接持有 FBWrapper 单例,无需在 service.json 再次注册。
 
@@ -9,7 +9,7 @@ AppDelegate 直接持有 FBWrapper 单例,无需在 service.json 再次注册
 1. 直接维护本目录的 `Info.plist`,然后在 Creator 构建并生成 Xcode 工程。sdk-manager 已弃用,不依赖构建面板配置。
    `FacebookAppID`、`FacebookClientToken` 已沿用 Android 当前配置;`FacebookAutoLogAppEventsEnabled` 为 `false`,与 Android 一致。
    修改 App ID 时,必须同时更新 `CFBundleURLTypes` 中的 `fb<AppID>`。保留 `LSApplicationQueriesSchemes` 中的 `fbapi`、`fbauth2` 和 `fb-messenger-share-api`。
-   `NSUserTrackingUsageDescription` 用于首次登录时的 ATT 系统弹窗,发布前确认说明与实际数据用途一致。
+   登录不请求 ATT 授权;只读取已有授权状态,拒绝或尚未授权不会阻断 Facebook 授权。
 2. 安装 CocoaPods 后,在本目录执行(替换为生成工程的实际绝对路径):
 
    ```sh
@@ -43,20 +43,29 @@ FBWrapper 的注册入口使用不捕获 self 或局部变量的全局 Block,
 
 ## 登录结果与后端
 
-当前使用 `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 用于授权后的令牌/用户信息验证。
+安装了 Facebook App(fbapi 和 fbauth2 检测通过)时启用 `FBSDKAppSwitchEnabled`,由官方 SDK 优先尝试 App 登录。
+没有安装时使用 `FBSDKLoginTrackingLimited` + `FBSDKAppSwitchDisabled`,直接进入 Limited Login 网页。
+有 App 且系统已允许追踪时可以使用普通登录;拒绝、受限或尚未授权时使用 Limited Login,不弹 ATT,不拦截授权。
+iOS 14–16 只同步真实的已有 ATT 状态;iOS 17+ 由 SDK 自行读取。取消登录保持取消,不自动再次弹出另一个登录窗口。
+
+安装 App 不等于一定能够跳 App:SDK 18.1.1 还检查 Meta 的普通/受限登录 Fast App Switch 功能开关。
+若功能未开放或跳转失败,SDK 会回退网页;Limited Login 会进入 limited.facebook.com。客户端不能保证或绕过这些条件。
+参考:https://github.com/facebook/facebook-ios-sdk/blob/v18.1.1/FBSDKLoginKit/FBSDKLoginKit/Internal/NativeAppLoginHandler.swift
+
+普通登录返回 `accessToken` 和 `tokenType: 'access'`,继续现有服务器登录接口。
+Limited Login 返回 `authenticationToken`(JWT)、`nonce` 和 `tokenType: 'authentication'`,`accessToken` 为空,不调用 Graph API `/me`。
+`status: 'connected'` 只表示 Facebook 授权成功,不代表已经取得游戏服务器会话。
+
+用户已确认当前后端不支持 JWT,因此 Limited Login 的游戏账号登录尚未接通。LoginService 会明确提示服务器未支持,不把 JWT 当 Access Token 发送。
+后端需要增加 OIDC/JWT 验证,校验签名、issuer、audience/App ID、有效期和与登录请求绑定的 nonce,并防止重放;随后约定客户端请求字段再接通。
+客户端无法把 Limited Login JWT 换成可供旧 Graph API 接口验证的普通 Access Token。两种模式都需要服务器独立验证身份。
 
 ## 验证清单
 
 - 初始化失败能返回配置错误;正常初始化后 isInitialized 为 true。
 - 真机登录成功/取消/失败;浏览器或 Facebook App 返回游戏;登出后可再次登录。
-- 首次 ATT 允许/拒绝、已拒绝和系统限制场景;允许后 Graph API 成功并继续服务器登录,其他情况可使用其他登录方式。
-- Graph API 网络失败或令牌不可用时应显示错误,并可重试;日志中不得包含完整令牌。
+- 已安装/未安装 Facebook App,分别结合 ATT 已允许/已拒绝/未确定状态测试;均不应弹 ATT 或因拒绝而阻断 Facebook 授权。
+- 普通 Access Token 继续服务器登录;JWT 正确回传 nonce,并显示后端尚未支持提示;日志中不得包含完整令牌。
 - 链接分享成功/取消/失败;重复操作被拒绝。
 - Meta 测试事件里检查 logEvent、logPurchase 和 flushEvents;返回 true 仅代表本地 SDK 接受调用。
 - 自动应用事件由 FacebookAutoLogAppEventsEnabled 控制;手动事件仍由业务显式调用。

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

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

+ 38 - 79
native/engine/ios/service/FBWrapper.mm

@@ -123,98 +123,57 @@
         [self reply:request data:nil cancelled:NO error:@"已有 Facebook 操作进行中,或没有可用窗口。"];
         return;
     }
-    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;
+    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://"]];
+    BOOL trackingAllowed = [FBSDKSettings sharedSettings].isAdvertiserTrackingEnabled;
     if (@available(iOS 14.0, *)) {
-        BOOL authorized = ATTrackingManager.trackingAuthorizationStatus == ATTrackingManagerAuthorizationStatusAuthorized;
+        // Read only. Login must never prompt for ATT or be blocked by a denial.
+        trackingAllowed = ATTrackingManager.trackingAuthorizationStatus == ATTrackingManagerAuthorizationStatusAuthorized;
         if (@available(iOS 17.0, *)) {
-            // SDK 17+ reads the actual ATT status itself on iOS 17+.
+            // SDK reads ATT itself.
         } else {
-            [FBSDKSettings sharedSettings].isAdvertiserTrackingEnabled = authorized;
-        }
-        if (!authorized) {
-            self.loginPending = NO;
-            [self reply:request data:nil cancelled:NO error:@"当前隐私授权状态无法使用此 Facebook Graph API 登录方式,请使用其他登录方式。"];
-            return;
+            [FBSDKSettings sharedSettings].isAdvertiserTrackingEnabled = trackingAllowed;
         }
     }
-    @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 {
+    // Prefer the installed app. Without existing tracking consent, native login
+    // can still use Limited Login when Meta enables Limited Login App Switch.
+    // No installed app always uses Limited Login in the browser.
+    FBSDKLoginTracking tracking = hasFacebookApp && trackingAllowed
+        ? FBSDKLoginTrackingEnabled : FBSDKLoginTrackingLimited;
+    NSString *nonce = NSUUID.UUID.UUIDString;
     FBSDKLoginConfiguration *config = [[FBSDKLoginConfiguration alloc]
-        initWithPermissions:@[@"public_profile", @"email"]
-        tracking:FBSDKLoginTrackingEnabled nonce:NSUUID.UUID.UUIDString];
+        initWithPermissions:@[@"public_profile", @"email"] tracking:tracking nonce:nonce
+        appSwitch:hasFacebookApp ? FBSDKAppSwitchEnabled : FBSDKAppSwitchDisabled];
+    if (!config) {
+        [self reply:request data:nil cancelled:NO error:@"Facebook 登录配置创建失败。"];
+        return;
+    }
+    self.loginPending = YES;
     [self.loginManager logInFromViewController:self.presenter configuration:config
         completion:^(FBSDKLoginManagerLoginResult *result, NSError *error) {
-        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) {
+        dispatch_async(dispatch_get_main_queue(), ^{
             self.loginPending = NO;
-            [self reply:request data:nil cancelled:NO error:exception.reason ?: @"Facebook Graph API 调用失败。"];
-        }
+            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;
+            if (tracking == FBSDKLoginTrackingEnabled && accessToken.length) {
+                [self reply:request data:@{@"accessToken": accessToken, @"tokenType": @"access"} cancelled:NO error:nil];
+            } else if (authenticationToken.length) {
+                [self reply:request data:@{@"authenticationToken": authenticationToken,
+                    @"tokenType": @"authentication", @"nonce": nonce} cancelled:NO error:nil];
+            } else {
+                [self reply:request data:nil cancelled:NO error:@"Facebook 登录未返回有效令牌。"];
+            }
+        });
     }];
 }