baxi пре 6 дана
родитељ
комит
af84b6fdd6

+ 3 - 0
assets/scripts/app/AppKernel.ts

@@ -109,6 +109,9 @@ export class AppKernel {
 
   private constructor() {
     this.platform = this.createPlatformBridge();
+    if (IOS && this.platform instanceof IOSPlatformBridge) {
+      this.platform.requestTrackingAuthorization();
+    }
     this.fb.init();
     this.google.init();
   }

+ 15 - 3
assets/scripts/platform/IOSPlatformBridge.ts

@@ -6,12 +6,24 @@ export class IOSPlatformBridge extends PlatformBridge {
   readonly type = PlatformType.IOS;
   private _deviceID: string | null = null;
 
+  /** 请求 ATT 系统授权;异步展示,不阻塞初始化,也不强制要求用户同意。 */
+  requestTrackingAuthorization(): void {
+    try {
+      const iosReflection = native.reflection as unknown as {
+        callStaticMethod(className: string, methodName: string): unknown;
+      };
+      iosReflection.callStaticMethod('CocosIOSPlatformBridge', 'requestTrackingAuthorization');
+    } catch (error) {
+      console.error('[IOS] 请求 ATT 授权失败:', error);
+    }
+  }
+
   getUrlParam(_name: string): string | null {
     console.error('[IOS] 获取深度链接参数能力未实现');
     return null;
   }
 
-  /** IDFV:同一开发商范围内的设备标识,不保证卸载重装后保持不变。 */
+  /** 首次生成 UUID 并存入本机 Keychain,后续读取;不是硬件编号。 */
   getDeviceID(): string | null {
     if (this._deviceID) return this._deviceID;
 
@@ -29,10 +41,10 @@ export class IOSPlatformBridge extends PlatformBridge {
         return this._deviceID;
       }
     } catch (error) {
-      console.error('[IOS] 获取 IDFV 失败:', error);
+      console.error('[IOS] 获取 Keychain 设备码失败:', error);
     }
 
-    // IDFV 在设备重启后首次解锁前可能不可用;不缓存失败,下次调用重试。
+    // Keychain 暂不可用或写入失败时不缓存结果,下次调用重试。
     return null;
   }
 }

+ 9 - 8
assets/scripts/services/login/LoginService.ts

@@ -17,15 +17,16 @@ class LoginService {
     async fbLogin() {
         const res = await AppKernel.fb.login();
         if (res.isCancel) return false;
-        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;
+        const token = res.accessToken || res.authenticationToken;
+        if (token) {
+            return await this.login3rd('facebook', token);
         }
+        // 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;
     }

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

@@ -23,6 +23,8 @@ 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")
 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")
+target_link_libraries(${EXECUTABLE_NAME} "-framework Security")
+target_link_libraries(${EXECUTABLE_NAME} "-weak_framework AppTrackingTransparency")
 set_target_properties(${EXECUTABLE_NAME} PROPERTIES
     XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET "12.0"
     XCODE_ATTRIBUTE_ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES "YES")

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

@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>NSUserTrackingUsageDescription</key>
+	<string>经您允许,我们将使用设备标识符衡量广告效果并提供个性化广告。</string>
 	<key>FacebookAppID</key>
 	<string>1056149470661855</string>
 	<key>FacebookClientToken</key>

+ 3 - 1
native/engine/ios/service/CocosIOSPlatformBridge.h

@@ -3,8 +3,10 @@
 NS_ASSUME_NONNULL_BEGIN
 
 @interface CocosIOSPlatformBridge : NSObject
-// Called synchronously by native.reflection; nil means temporarily unavailable.
+// App-scoped UUID persisted in this device's Keychain. nil means read/write failed.
 + (nullable NSString *)getDeviceID;
+// Schedules the system ATT prompt on the main thread once the app is active.
++ (void)requestTrackingAuthorization;
 @end
 
 NS_ASSUME_NONNULL_END

+ 100 - 3
native/engine/ios/service/CocosIOSPlatformBridge.mm

@@ -1,12 +1,109 @@
 #import "CocosIOSPlatformBridge.h"
+#import <Security/Security.h>
 #import <UIKit/UIKit.h>
+#import <AppTrackingTransparency/AppTrackingTransparency.h>
 
 @implementation CocosIOSPlatformBridge
 
++ (void)requestTrackingAuthorization {
+    dispatch_async(dispatch_get_main_queue(), ^{
+        // All accesses are serialized on the main queue.
+        static BOOL requestInFlight = NO;
+        static id activeObserver = nil;
+        if (@available(iOS 14, *)) {
+            if (requestInFlight) return;
+            NSString *usage = [NSBundle.mainBundle objectForInfoDictionaryKey:@"NSUserTrackingUsageDescription"];
+            if (![usage isKindOfClass:NSString.class] || !usage.length) {
+                NSLog(@"[ATT] Missing NSUserTrackingUsageDescription; request skipped.");
+                return;
+            }
+            if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
+                if (!activeObserver) {
+                    activeObserver = [NSNotificationCenter.defaultCenter
+                        addObserverForName:UIApplicationDidBecomeActiveNotification
+                        object:nil queue:NSOperationQueue.mainQueue
+                        usingBlock:^(NSNotification *notification) {
+                            [NSNotificationCenter.defaultCenter removeObserver:activeObserver];
+                            activeObserver = nil;
+                            [CocosIOSPlatformBridge requestTrackingAuthorization];
+                        }];
+                }
+                return;
+            }
+            if (activeObserver) {
+                [NSNotificationCenter.defaultCenter removeObserver:activeObserver];
+                activeObserver = nil;
+            }
+            ATTrackingManagerAuthorizationStatus status = ATTrackingManager.trackingAuthorizationStatus;
+            if (status != ATTrackingManagerAuthorizationStatusNotDetermined) {
+                NSLog(@"[ATT] Existing status: %ld", (long)status);
+                return;
+            }
+            requestInFlight = YES;
+            [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus result) {
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    requestInFlight = NO;
+                    // 0=notDetermined, 1=restricted, 2=denied, 3=authorized.
+                    NSLog(@"[ATT] Authorization status: %ld", (long)result);
+                });
+            }];
+        } else {
+            NSLog(@"[ATT] Not available before iOS 14.");
+        }
+    });
+}
+
 + (nullable NSString *)getDeviceID {
-    // Do not persist IDFV in backed-up storage: a restored backup can belong to
-    // another device. Always obtain this device's identifier from the OS.
-    return UIDevice.currentDevice.identifierForVendor.UUIDString;
+    @synchronized(self) {
+        NSString *bundleID = NSBundle.mainBundle.bundleIdentifier;
+        if (!bundleID.length) return nil;
+
+        // Keep these keys stable between releases to preserve the identifier.
+        NSDictionary *key = @{
+            (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
+            (__bridge id)kSecAttrService: [bundleID stringByAppendingString:@".device-identifier"],
+            (__bridge id)kSecAttrAccount: @"deviceUUID.v1",
+            (__bridge id)kSecAttrSynchronizable: @NO,
+        };
+        // Retry a read if another process created the item between our read/add.
+        for (NSUInteger attempt = 0; attempt < 2; ++attempt) {
+            NSMutableDictionary *query = [key mutableCopy];
+            query[(__bridge id)kSecReturnData] = @YES;
+            query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
+            CFTypeRef result = NULL;
+            OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
+            id data = CFBridgingRelease(result);
+            if (status == errSecSuccess) {
+                NSString *savedUUID = [data isKindOfClass:NSData.class]
+                    ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil;
+                if (savedUUID.length && [[NSUUID alloc] initWithUUIDString:savedUUID]) {
+                    return savedUUID;
+                }
+                // A damaged value must not silently replace an existing identity.
+                NSLog(@"[iOS DeviceID] Invalid Keychain UUID; keeping the existing item.");
+                return nil;
+            }
+            if (status != errSecItemNotFound) {
+                // Includes a locked/unavailable Keychain. Do not create a new ID.
+                NSLog(@"[iOS DeviceID] Keychain read failed: %d", (int)status);
+                return nil;
+            }
+
+            NSString *uuid = NSUUID.UUID.UUIDString;
+            NSMutableDictionary *item = [key mutableCopy];
+            item[(__bridge id)kSecValueData] = [uuid dataUsingEncoding:NSUTF8StringEncoding];
+            // No iCloud sync or migration to another device through backups.
+            item[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
+            status = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
+            if (status == errSecSuccess) return uuid;
+            if (status != errSecDuplicateItem) {
+                NSLog(@"[iOS DeviceID] Keychain write failed: %d", (int)status);
+                return nil;
+            }
+        }
+        NSLog(@"[iOS DeviceID] Keychain item changed concurrently; retry on next call.");
+        return nil;
+    }
 }
 
 @end