Files
keyboard/keyBoard/Class/Manager/AppleSignInManager.m
2025-11-17 20:07:39 +08:00

179 lines
7.6 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// AppleSignInManager.m
// 封装“用 Apple 登录”的实现与存储
#import "AppleSignInManager.h"
#import <UIKit/UIKit.h>
#import <Security/Security.h>
static NSString * const kKBAppleUserIdentifierKey = @"com.company.keyboard.apple.user"; // 钥匙串键名
@interface AppleSignInManager ()
@property (nonatomic, weak) UIViewController *presentingVC;
@property (nonatomic, copy) KBAppleSignInCompletion completion;
@end
@implementation AppleSignInManager
+ (instancetype)shared {
static AppleSignInManager *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ instance = [AppleSignInManager new]; });
return instance;
}
- (NSString *)storedUserIdentifier {
return [self.class keychainLoad:kKBAppleUserIdentifierKey];
}
- (void)signInFromViewController:(UIViewController *)presenting completion:(KBAppleSignInCompletion)completion {
if (!NSThread.isMainThread) {
// 确保在主线程发起,否则可能得到 Unknown(1000)
dispatch_async(dispatch_get_main_queue(), ^{ [self signInFromViewController:presenting completion:completion]; });
return;
}
if (@available(iOS 13.0, *)) {
self.presentingVC = presenting;
self.completion = completion;
ASAuthorizationAppleIDProvider *provider = [ASAuthorizationAppleIDProvider new];
ASAuthorizationAppleIDRequest *request = provider.createRequest;
request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
ASAuthorizationController *controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
controller.delegate = self;
controller.presentationContextProvider = self;
[controller performRequests];
} else {
if (completion) {
NSError *err = [NSError errorWithDomain:@"AppleSignIn" code:-1 userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Sign in with Apple requires iOS 13 or later")}];
completion(nil, err);
}
}
}
- (void)checkCredentialStateWithCompletion:(void(^)(ASAuthorizationAppleIDProviderCredentialState state))completion {
if (!completion) return;
if (@available(iOS 13.0, *)) {
NSString *userID = self.storedUserIdentifier;
if (!userID) {
completion(ASAuthorizationAppleIDProviderCredentialNotFound);
return;
}
ASAuthorizationAppleIDProvider *provider = [ASAuthorizationAppleIDProvider new];
[provider getCredentialStateForUserID:userID completion:^(ASAuthorizationAppleIDProviderCredentialState credentialState, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{ completion(credentialState); });
}];
} else {
completion(ASAuthorizationAppleIDProviderCredentialNotFound);
}
}
// 使用指定的 userIdentifier 检查凭证状态(无需本地存储)
- (void)checkCredentialStateForUserID:(NSString *)userID completion:(void(^)(ASAuthorizationAppleIDProviderCredentialState state))completion {
if (!completion) return;
if (@available(iOS 13.0, *)) {
if (userID.length == 0) {
completion(ASAuthorizationAppleIDProviderCredentialNotFound);
return;
}
ASAuthorizationAppleIDProvider *provider = [ASAuthorizationAppleIDProvider new];
[provider getCredentialStateForUserID:userID completion:^(ASAuthorizationAppleIDProviderCredentialState credentialState, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{ completion(credentialState); });
}];
} else {
completion(ASAuthorizationAppleIDProviderCredentialNotFound);
}
}
#pragma mark - 授权回调 (ASAuthorizationControllerDelegate)
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
if (@available(iOS 13.0, *)) {
ASAuthorizationAppleIDCredential *credential = authorization.credential;
// 为了让键盘扩展/主 App 能识别“是否已登录”,在本地持久化 userIdentifier 到钥匙串。
// 仅保存一个标记(不包含敏感信息),业务登录态仍以服务端为准。
NSString *userID = credential.user ?: @"";
if (userID.length > 0) {
[self.class keychainSave:kKBAppleUserIdentifierKey value:userID];
}
if (self.completion) {
self.completion(credential, nil);
}
}
self.completion = nil;
self.presentingVC = nil;
}
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {
if (self.completion) {
self.completion(nil, error);
}
self.completion = nil;
self.presentingVC = nil;
}
#pragma mark - 授权界面展示锚点 (ASAuthorizationControllerPresentationContextProviding)
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)) {
// 优先用传入 VC 的 window
UIWindow *win = self.presentingVC.view.window;
if (win) return win;
// iOS13+ 从前台激活的 scene 中取 keyWindow
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive && [scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *ws = (UIWindowScene *)scene;
for (UIWindow *w in ws.windows) { if (w.isKeyWindow) return w; }
if (ws.windows.firstObject) return ws.windows.firstObject;
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.windows.firstObject;
#pragma clang diagnostic pop
}
#pragma mark - Keychain 工具
// 本地登出:删除已存储的 userIdentifier使 App 重新要求登录
- (void)signOut {
NSDictionary *query = @{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: kKBAppleUserIdentifierKey,
(__bridge id)kSecAttrAccount: kKBAppleUserIdentifierKey};
SecItemDelete((__bridge CFDictionaryRef)query);
}
+ (BOOL)keychainSave:(NSString *)key value:(NSString *)value {
if (!key) return NO;
NSData *data = [value dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *query = @{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: key,
(__bridge id)kSecAttrAccount: key};
SecItemDelete((__bridge CFDictionaryRef)query);
NSMutableDictionary *attributes = [query mutableCopy];
attributes[(__bridge id)kSecValueData] = data ?: [NSData data];
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
return (status == errSecSuccess);
}
+ (NSString *)keychainLoad:(NSString *)key {
if (!key) return nil;
NSDictionary *query = @{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: key,
(__bridge id)kSecAttrAccount: key,
(__bridge id)kSecReturnData: @YES,
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne};
CFTypeRef dataRef = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef);
if (status != errSecSuccess || !dataRef) return nil;
NSData *data = (__bridge_transfer NSData *)dataRef;
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
@end