baxi 4 өдөр өмнө
parent
commit
01e69b5f45

+ 3 - 0
assets/lobbyRes/ui/login/LoginView.prefab

@@ -5780,6 +5780,9 @@
     "btnGoogle": {
     "btnGoogle": {
       "__id__": 170
       "__id__": 170
     },
     },
+    "btnApple": {
+      "__id__": 190
+    },
     "loginAccount": {
     "loginAccount": {
       "__id__": 49
       "__id__": 49
     },
     },

+ 9 - 1
assets/scripts/app/AppKernel.ts

@@ -1,4 +1,4 @@
-import { ANDROID, HTML5, IOS } from 'cc/env';
+import { ANDROID, IOS } from 'cc/env';
 import type { AppEventMap } from './AppEvents';
 import type { AppEventMap } from './AppEvents';
 import { EventManager } from '../framework/event/EventManager';
 import { EventManager } from '../framework/event/EventManager';
 import { ResourceManager } from '../framework/resource/ResourceManager';
 import { ResourceManager } from '../framework/resource/ResourceManager';
@@ -16,6 +16,8 @@ import * as stores from '../store';
 import * as services from '../services';
 import * as services from '../services';
 import fb from '@scripts/platform/FaceBook/Facebook';
 import fb from '@scripts/platform/FaceBook/Facebook';
 import google from '@scripts/platform/Google/Google';
 import google from '@scripts/platform/Google/Google';
+import iosAuth from '@scripts/platform/IOS/IOSAuth';
+import iap from '@scripts/platform/IAP/IAP';
 import * as i18n from '../i18n';
 import * as i18n from '../i18n';
 
 
 
 
@@ -92,6 +94,10 @@ export class AppKernel {
 
 
   static get i18n(): typeof i18n { return i18n; }
   static get i18n(): typeof i18n { return i18n; }
 
 
+  static get iap(): typeof iap { return iap; }
+
+  static get iosAuth(): typeof iosAuth { return iosAuth; }
+
   readonly events = new EventManager<AppEventMap>();
   readonly events = new EventManager<AppEventMap>();
   readonly redpoint = new RedpointManager();
   readonly redpoint = new RedpointManager();
   readonly resources = new ResourceManager();
   readonly resources = new ResourceManager();
@@ -106,6 +112,8 @@ export class AppKernel {
   readonly fb = fb;
   readonly fb = fb;
   readonly google = google;
   readonly google = google;
   readonly i18n = i18n;
   readonly i18n = i18n;
+  readonly iap = iap;
+  readonly iosAuth = iosAuth;
 
 
   private constructor() {
   private constructor() {
     this.platform = this.createPlatformBridge();
     this.platform = this.createPlatformBridge();

+ 10 - 0
assets/scripts/modules/lobby/ui/LoginView/LoginView.ts

@@ -21,6 +21,9 @@ export class LoginView extends UIView {
   @property(Button)
   @property(Button)
   btnGoogle: Button;
   btnGoogle: Button;
 
 
+  @property(Button)
+  btnApple: Button;
+
   @property(EditBox)
   @property(EditBox)
   loginAccount: EditBox;
   loginAccount: EditBox;
 
 
@@ -60,6 +63,7 @@ export class LoginView extends UIView {
     addBtnClick(this.btnGuest.node, this.onBtnGuestClick, this);
     addBtnClick(this.btnGuest.node, this.onBtnGuestClick, this);
     addBtnClick(this.btnFacebook.node, this.onBtnFBClick, this);
     addBtnClick(this.btnFacebook.node, this.onBtnFBClick, this);
     addBtnClick(this.btnGoogle.node, this.onBtnGoogleClick, this);
     addBtnClick(this.btnGoogle.node, this.onBtnGoogleClick, this);
+    addBtnClick(this.btnApple.node, this.onBtnAppleClick, this);
     this._googleWebBtn = AppKernel.google.createLoginBtn(
     this._googleWebBtn = AppKernel.google.createLoginBtn(
       this.btnGoogle.node,
       this.btnGoogle.node,
       res => this.onGoogleWebLogin(res),
       res => this.onGoogleWebLogin(res),
@@ -189,6 +193,12 @@ export class LoginView extends UIView {
     AppKernel.ui.loading(false);
     AppKernel.ui.loading(false);
   }
   }
 
 
+  private async onBtnAppleClick() {
+    AppKernel.ui.loading(true);
+    await AppKernel.services.loginService.iosAuthLogin();
+    AppKernel.ui.loading(false);
+  }
+
   private onBtnRegisterClick() {
   private onBtnRegisterClick() {
     // this._googleWebBtn?.setActive(false);
     // this._googleWebBtn?.setActive(false);
     AppKernel.ui.open(UIViewID.Register);
     AppKernel.ui.open(UIViewID.Register);

+ 1 - 5
assets/scripts/platform/FaceBook/FacebookImpl.ts

@@ -76,7 +76,7 @@ export abstract class FacebookImpl {
 
 
     protected onInited() {
     protected onInited() {
         this._isInitialized = true;
         this._isInitialized = true;
-        this.activeApp();
+        this.onLogEvent(FBEventNames.ActivatedApp);
     }
     }
 
 
     async login(): Promise<IFBLoginRes> {
     async login(): Promise<IFBLoginRes> {
@@ -90,10 +90,6 @@ export abstract class FacebookImpl {
 
 
     abstract share(options: IFBShareOptions): Promise<IFBShareRes>;
     abstract share(options: IFBShareOptions): Promise<IFBShareRes>;
 
 
-    activeApp() {
-        this.logEvent(FBEventNames.ActivatedApp);
-    }
-
     logEvent(eventName: string, parameters?: IFBEventParams, valueToSum?: number): Promise<boolean> {
     logEvent(eventName: string, parameters?: IFBEventParams, valueToSum?: number): Promise<boolean> {
         const fbEvent = EventsMap[eventName]
         const fbEvent = EventsMap[eventName]
         if (!!fbEvent) {
         if (!!fbEvent) {

+ 9 - 0
assets/scripts/platform/IOS.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "1.2.0",
+  "importer": "directory",
+  "imported": true,
+  "uuid": "43af0950-bba3-4bb2-bfc3-721ac7836209",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

+ 59 - 0
assets/scripts/platform/IOS/IOSAuth.ts

@@ -0,0 +1,59 @@
+import { IOS } from "cc/env";
+import { native } from 'cc';
+
+export interface IOSAuthResult {
+    status: 'connected' | 'cancelled' | 'error';
+    isCancel: boolean;
+    identityToken?: string;
+    authorizationCode?: string;
+    userId?: string;
+    name?: string;
+    email?: string;
+    nonce?: string;
+    reason?: string;
+}
+
+class IOSAuth {
+    private listening = false;
+    private seed = 0;
+    private pending: { id: string; resolve: (result: IOSAuthResult) => void; timer: ReturnType<typeof setTimeout> } | null = null;
+
+    init(): void {
+        if (this.listening) return;
+        native.jsbBridgeWrapper.addNativeEventListener('appleauth.response', payload => {
+            let response: { requestId: string; data: IOSAuthResult };
+            try { response = JSON.parse(payload); } catch { return; }
+            const pending = this.pending;
+            if (!pending || response.requestId !== pending.id) return;
+            clearTimeout(pending.timer);
+            this.pending = null;
+            pending.resolve(response.data);
+        });
+        this.listening = true;
+    }
+
+    /** nonce 原样传给 Apple;生产环境建议使用后端预先生成并绑定本次登录的随机挑战。 */
+    async login(nonce?: string): Promise<IOSAuthResult> {
+        const failure = (reason: string): IOSAuthResult => ({ status: 'error', isCancel: false, reason });
+        if (this.pending) return failure('Apple 登录正在进行中。');
+        if (nonce !== undefined && (!nonce.trim() || nonce.length > 512)) return failure('nonce 无效。');
+        try { this.init(); } catch (error) { return failure(String(error)); }
+        const id = `${Date.now()}-${++this.seed}`;
+        return new Promise(resolve => {
+            const timer = setTimeout(() => {
+                this.pending = null;
+                resolve(failure('Apple 授权等待超时,请关闭系统授权窗口后重试。'));
+            }, 180000);
+            this.pending = { id, resolve, timer };
+            try {
+                native.jsbBridgeWrapper.dispatchEventToNative('appleauth.request', JSON.stringify({ requestId: id, nonce }));
+            } catch (error) {
+                clearTimeout(timer); this.pending = null; resolve(failure(String(error)));
+            }
+        });
+    }
+}
+
+
+const iosAuth = IOS ? new IOSAuth : null;
+export default iosAuth;

+ 9 - 0
assets/scripts/platform/IOS/IOSAuth.ts.meta

@@ -0,0 +1,9 @@
+{
+  "ver": "4.0.24",
+  "importer": "typescript",
+  "imported": true,
+  "uuid": "823f092b-152f-47b9-a374-e8ad39fea48f",
+  "files": [],
+  "subMetas": {},
+  "userData": {}
+}

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

@@ -43,6 +43,15 @@ class LoginService {
         return true;
         return true;
     }
     }
 
 
+    async iosAuthLogin() {
+        if (!AppKernel.iosAuth) return false;
+        const res = await AppKernel.iosAuth.login();
+        console.log(`ios auth res: ${JSON.stringify(res)}`)
+        if (res.status === 'connected') {
+
+        }
+    }
+
     async login3rd(type: Login3rdType, token: string) {
     async login3rd(type: Login3rdType, token: string) {
         const mainStore = AppKernel.stores.mainStore;
         const mainStore = AppKernel.stores.mainStore;
         const res = await mainStore.reqLogin3rd(type, token);
         const res = await mainStore.reqLogin3rd(type, token);

+ 2 - 0
native/engine/ios/AppDelegate.mm

@@ -35,6 +35,7 @@
 #import "service/FBWrapper.h"
 #import "service/FBWrapper.h"
 #import "service/GoogleWrapper.h"
 #import "service/GoogleWrapper.h"
 #import "service/IAPWrapper.h"
 #import "service/IAPWrapper.h"
+#import "service/AppleAuthWrapper.h"
 
 
 @implementation AppDelegate
 @implementation AppDelegate
 @synthesize window;
 @synthesize window;
@@ -47,6 +48,7 @@
     [[FBWrapper shared] application:application didFinishLaunchingWithOptions:launchOptions];
     [[FBWrapper shared] application:application didFinishLaunchingWithOptions:launchOptions];
     [[GoogleWrapper shared] registerBridge];
     [[GoogleWrapper shared] registerBridge];
     [[IAPWrapper shared] start];
     [[IAPWrapper shared] start];
+    [[AppleAuthWrapper shared] start];
     [[SDKWrapper shared] application:application didFinishLaunchingWithOptions:launchOptions];
     [[SDKWrapper shared] application:application didFinishLaunchingWithOptions:launchOptions];
     appDelegateBridge = [[AppDelegateBridge alloc] init];
     appDelegateBridge = [[AppDelegateBridge alloc] init];
     
     

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

@@ -18,6 +18,9 @@ set(EXECUTABLE_NAME ${APP_NAME}-mobile)
 cc_ios_before_target(${EXECUTABLE_NAME})
 cc_ios_before_target(${EXECUTABLE_NAME})
 add_executable(${EXECUTABLE_NAME} ${CC_ALL_SOURCES})
 add_executable(${EXECUTABLE_NAME} ${CC_ALL_SOURCES})
 cc_ios_after_target(${EXECUTABLE_NAME})
 cc_ios_after_target(${EXECUTABLE_NAME})
+target_sources(${EXECUTABLE_NAME} PRIVATE ${CC_PROJECT_DIR}/service/AppleAuthWrapper.mm)
+set_source_files_properties(${CC_PROJECT_DIR}/service/AppleAuthWrapper.mm PROPERTIES COMPILE_FLAGS "-fobjc-arc")
+target_link_libraries(${EXECUTABLE_NAME} "-framework AuthenticationServices")
 target_sources(${EXECUTABLE_NAME} PRIVATE ${CC_PROJECT_DIR}/service/IAPWrapper.mm)
 target_sources(${EXECUTABLE_NAME} PRIVATE ${CC_PROJECT_DIR}/service/IAPWrapper.mm)
 set_source_files_properties(${CC_PROJECT_DIR}/service/IAPWrapper.mm PROPERTIES COMPILE_FLAGS "-fobjc-arc")
 set_source_files_properties(${CC_PROJECT_DIR}/service/IAPWrapper.mm PROPERTIES COMPILE_FLAGS "-fobjc-arc")
 target_link_libraries(${EXECUTABLE_NAME} "-framework StoreKit")
 target_link_libraries(${EXECUTABLE_NAME} "-framework StoreKit")

+ 5 - 0
native/engine/ios/service/AppleAuthWrapper.h

@@ -0,0 +1,5 @@
+#import <Foundation/Foundation.h>
+@interface AppleAuthWrapper : NSObject
++ (instancetype)shared;
+- (void)start;
+@end

+ 106 - 0
native/engine/ios/service/AppleAuthWrapper.mm

@@ -0,0 +1,106 @@
+#import "AppleAuthWrapper.h"
+#import <UIKit/UIKit.h>
+#import <AuthenticationServices/AuthenticationServices.h>
+#import "platform/apple/JsbBridgeWrapper.h"
+#include "application/ApplicationManager.h"
+#include "application/BaseApplication.h"
+#include "base/Scheduler.h"
+#include "engine/BaseEngine.h"
+
+@interface AppleAuthWrapper () <ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding>
+@property(nonatomic, strong) ASAuthorizationController *controller API_AVAILABLE(ios(13.0));
+@property(nonatomic, strong) UIWindow *anchor;
+@property(nonatomic, copy) NSString *requestID;
+@property(nonatomic, copy) NSString *nonce;
+@property(nonatomic, copy) NSString *state;
+@end
+@implementation AppleAuthWrapper
++ (instancetype)shared {
+    static AppleAuthWrapper *instance;
+    static dispatch_once_t once;
+    dispatch_once(&once, ^{ instance = [AppleAuthWrapper new]; });
+    return instance;
+}
+- (void)start {
+    // Capture-free global block required by Creator 3.8.8's MRC bridge.
+    static OnScriptEventListener const listener = ^(NSString *payload) {
+        dispatch_async(dispatch_get_main_queue(), ^{ [[AppleAuthWrapper shared] login:payload]; });
+    };
+    [[JsbBridgeWrapper sharedInstance] addScriptEventListener:@"appleauth.request" listener:listener];
+}
+- (void)reply:(NSString *)requestID data:(NSDictionary *)data {
+    NSData *json = [NSJSONSerialization dataWithJSONObject:@{@"requestId": requestID, @"data": data} 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:@"appleauth.response" arg:payload];
+    });
+}
+- (void)fail:(NSString *)requestID reason:(NSString *)reason {
+    [self reply:requestID data:@{@"status": @"error", @"isCancel": @NO, @"reason": reason}];
+}
+- (void)clear API_AVAILABLE(ios(13.0)) {
+    self.controller = nil; self.anchor = nil; self.requestID = nil; self.nonce = nil; self.state = nil;
+}
+- (void)login:(NSString *)payload {
+    id r = [NSJSONSerialization JSONObjectWithData:[payload dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
+    if (![r isKindOfClass:NSDictionary.class] || ![r[@"requestId"] isKindOfClass:NSString.class]) return;
+    NSString *requestID = r[@"requestId"];
+    if (@available(iOS 13.0, *)) {
+        if (self.controller) { [self fail:requestID reason:@"Apple 登录正在进行中,请先关闭已有授权窗口。"]; return; }
+        UIWindow *window = UIApplication.sharedApplication.delegate.window;
+        if (!window || UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
+            [self fail:requestID reason:@"请在应用前台、系统授权弹窗关闭后重试。"]; return;
+        }
+        @try {
+            self.requestID = requestID;
+            self.anchor = window;
+            self.state = NSUUID.UUID.UUIDString;
+            self.nonce = [r[@"nonce"] isKindOfClass:NSString.class] && [r[@"nonce"] length] ? r[@"nonce"] : NSUUID.UUID.UUIDString;
+            ASAuthorizationAppleIDRequest *request = [[ASAuthorizationAppleIDProvider new] createRequest];
+            request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
+            request.nonce = self.nonce;
+            request.state = self.state;
+            self.controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
+            self.controller.delegate = self;
+            self.controller.presentationContextProvider = self;
+            [self.controller performRequests];
+        } @catch (NSException *exception) {
+            [self fail:requestID reason:exception.reason ?: @"Apple 授权调用失败。"];
+            [self clear];
+        }
+    } else { [self fail:requestID reason:@"通过 Apple 登录需要 iOS 13 或更新版本。"]; }
+}
+- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)) {
+    return self.anchor;
+}
+- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+        if (controller != self.controller) return;
+        if (![authorization.credential isKindOfClass:ASAuthorizationAppleIDCredential.class]) {
+            [self fail:self.requestID reason:@"Apple 返回了不支持的凭证类型。"]; [self clear]; return;
+        }
+        ASAuthorizationAppleIDCredential *credential = (ASAuthorizationAppleIDCredential *)authorization.credential;
+        NSString *token = credential.identityToken ? [[NSString alloc] initWithData:credential.identityToken encoding:NSUTF8StringEncoding] : nil;
+        NSString *code = credential.authorizationCode ? [[NSString alloc] initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding] : nil;
+        if (!token.length || !code.length || ![credential.state isEqualToString:self.state]) {
+            [self fail:self.requestID reason:@"Apple 凭证缺失或 state 校验失败。"]; [self clear]; return;
+        }
+        NSString *name = credential.fullName ? [NSPersonNameComponentsFormatter localizedStringFromPersonNameComponents:credential.fullName style:NSPersonNameComponentsFormatterStyleDefault options:0] : @"";
+        [self reply:self.requestID data:@{@"status": @"connected", @"isCancel": @NO,
+            @"identityToken": token, @"authorizationCode": code, @"userId": credential.user,
+            @"name": name ?: @"", @"email": credential.email ?: @"", @"nonce": self.nonce}];
+        [self clear];
+    });
+}
+- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+        if (controller != self.controller) return;
+        BOOL cancelled = [error.domain isEqualToString:ASAuthorizationErrorDomain] && error.code == ASAuthorizationErrorCanceled;
+        [self reply:self.requestID data:@{@"status": cancelled ? @"cancelled" : @"error", @"isCancel": @(cancelled),
+            @"reason": error.localizedDescription ?: @"Apple 授权失败。"}];
+        [self clear];
+    });
+}
+@end