#import "AppleAuthWrapper.h" #import #import #import "platform/apple/JsbBridgeWrapper.h" #include "application/ApplicationManager.h" #include "application/BaseApplication.h" #include "base/Scheduler.h" #include "engine/BaseEngine.h" @interface AppleAuthWrapper () @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