AppleAuthWrapper.mm 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. #import "AppleAuthWrapper.h"
  2. #import <UIKit/UIKit.h>
  3. #import <AuthenticationServices/AuthenticationServices.h>
  4. #import "platform/apple/JsbBridgeWrapper.h"
  5. #include "application/ApplicationManager.h"
  6. #include "application/BaseApplication.h"
  7. #include "base/Scheduler.h"
  8. #include "engine/BaseEngine.h"
  9. @interface AppleAuthWrapper () <ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding>
  10. @property(nonatomic, strong) ASAuthorizationController *controller API_AVAILABLE(ios(13.0));
  11. @property(nonatomic, strong) UIWindow *anchor;
  12. @property(nonatomic, copy) NSString *requestID;
  13. @property(nonatomic, copy) NSString *nonce;
  14. @property(nonatomic, copy) NSString *state;
  15. @end
  16. @implementation AppleAuthWrapper
  17. + (instancetype)shared {
  18. static AppleAuthWrapper *instance;
  19. static dispatch_once_t once;
  20. dispatch_once(&once, ^{ instance = [AppleAuthWrapper new]; });
  21. return instance;
  22. }
  23. - (void)start {
  24. // Capture-free global block required by Creator 3.8.8's MRC bridge.
  25. static OnScriptEventListener const listener = ^(NSString *payload) {
  26. dispatch_async(dispatch_get_main_queue(), ^{ [[AppleAuthWrapper shared] login:payload]; });
  27. };
  28. [[JsbBridgeWrapper sharedInstance] addScriptEventListener:@"appleauth.request" listener:listener];
  29. }
  30. - (void)reply:(NSString *)requestID data:(NSDictionary *)data {
  31. NSData *json = [NSJSONSerialization dataWithJSONObject:@{@"requestId": requestID, @"data": data} options:0 error:nil];
  32. NSString *payload = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding];
  33. auto app = CC_CURRENT_APPLICATION();
  34. if (!app || !app->getEngine() || !payload) return;
  35. app->getEngine()->getScheduler()->performFunctionInCocosThread([payload]() {
  36. [[JsbBridgeWrapper sharedInstance] dispatchEventToScript:@"appleauth.response" arg:payload];
  37. });
  38. }
  39. - (void)fail:(NSString *)requestID reason:(NSString *)reason {
  40. [self reply:requestID data:@{@"status": @"error", @"isCancel": @NO, @"reason": reason}];
  41. }
  42. - (void)clear API_AVAILABLE(ios(13.0)) {
  43. self.controller = nil; self.anchor = nil; self.requestID = nil; self.nonce = nil; self.state = nil;
  44. }
  45. - (void)login:(NSString *)payload {
  46. id r = [NSJSONSerialization JSONObjectWithData:[payload dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
  47. if (![r isKindOfClass:NSDictionary.class] || ![r[@"requestId"] isKindOfClass:NSString.class]) return;
  48. NSString *requestID = r[@"requestId"];
  49. if (@available(iOS 13.0, *)) {
  50. if (self.controller) { [self fail:requestID reason:@"Apple 登录正在进行中,请先关闭已有授权窗口。"]; return; }
  51. UIWindow *window = UIApplication.sharedApplication.delegate.window;
  52. if (!window || UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
  53. [self fail:requestID reason:@"请在应用前台、系统授权弹窗关闭后重试。"]; return;
  54. }
  55. @try {
  56. self.requestID = requestID;
  57. self.anchor = window;
  58. self.state = NSUUID.UUID.UUIDString;
  59. self.nonce = [r[@"nonce"] isKindOfClass:NSString.class] && [r[@"nonce"] length] ? r[@"nonce"] : NSUUID.UUID.UUIDString;
  60. ASAuthorizationAppleIDRequest *request = [[ASAuthorizationAppleIDProvider new] createRequest];
  61. request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
  62. request.nonce = self.nonce;
  63. request.state = self.state;
  64. self.controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
  65. self.controller.delegate = self;
  66. self.controller.presentationContextProvider = self;
  67. [self.controller performRequests];
  68. } @catch (NSException *exception) {
  69. [self fail:requestID reason:exception.reason ?: @"Apple 授权调用失败。"];
  70. [self clear];
  71. }
  72. } else { [self fail:requestID reason:@"通过 Apple 登录需要 iOS 13 或更新版本。"]; }
  73. }
  74. - (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)) {
  75. return self.anchor;
  76. }
  77. - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
  78. dispatch_async(dispatch_get_main_queue(), ^{
  79. if (controller != self.controller) return;
  80. if (![authorization.credential isKindOfClass:ASAuthorizationAppleIDCredential.class]) {
  81. [self fail:self.requestID reason:@"Apple 返回了不支持的凭证类型。"]; [self clear]; return;
  82. }
  83. ASAuthorizationAppleIDCredential *credential = (ASAuthorizationAppleIDCredential *)authorization.credential;
  84. NSString *token = credential.identityToken ? [[NSString alloc] initWithData:credential.identityToken encoding:NSUTF8StringEncoding] : nil;
  85. NSString *code = credential.authorizationCode ? [[NSString alloc] initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding] : nil;
  86. if (!token.length || !code.length || ![credential.state isEqualToString:self.state]) {
  87. [self fail:self.requestID reason:@"Apple 凭证缺失或 state 校验失败。"]; [self clear]; return;
  88. }
  89. NSString *name = credential.fullName ? [NSPersonNameComponentsFormatter localizedStringFromPersonNameComponents:credential.fullName style:NSPersonNameComponentsFormatterStyleDefault options:0] : @"";
  90. [self reply:self.requestID data:@{@"status": @"connected", @"isCancel": @NO,
  91. @"identityToken": token, @"authorizationCode": code, @"userId": credential.user,
  92. @"name": name ?: @"", @"email": credential.email ?: @"", @"nonce": self.nonce}];
  93. [self clear];
  94. });
  95. }
  96. - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {
  97. dispatch_async(dispatch_get_main_queue(), ^{
  98. if (controller != self.controller) return;
  99. BOOL cancelled = [error.domain isEqualToString:ASAuthorizationErrorDomain] && error.code == ASAuthorizationErrorCanceled;
  100. [self reply:self.requestID data:@{@"status": cancelled ? @"cancelled" : @"error", @"isCancel": @(cancelled),
  101. @"reason": error.localizedDescription ?: @"Apple 授权失败。"}];
  102. [self clear];
  103. });
  104. }
  105. @end