|
|
@@ -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
|