2025-10-30 14:29:11 +08:00
|
|
|
|
// 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 {
|
2025-11-13 14:11:44 +08:00
|
|
|
|
if (!NSThread.isMainThread) {
|
|
|
|
|
|
// 确保在主线程发起,否则可能得到 Unknown(1000)
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{ [self signInFromViewController:presenting completion:completion]; });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-30 14:29:11 +08:00
|
|
|
|
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: @"Apple 登录需要 iOS 13 及以上版本"}];
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 14:35:06 +08:00
|
|
|
|
// 使用指定的 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 14:29:11 +08:00
|
|
|
|
#pragma mark - 授权回调 (ASAuthorizationControllerDelegate)
|
|
|
|
|
|
|
|
|
|
|
|
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
|
|
|
|
|
|
if (@available(iOS 13.0, *)) {
|
|
|
|
|
|
ASAuthorizationAppleIDCredential *credential = authorization.credential;
|
2025-10-30 20:23:34 +08:00
|
|
|
|
// 为了让键盘扩展/主 App 能识别“是否已登录”,在本地持久化 userIdentifier 到钥匙串。
|
|
|
|
|
|
// 仅保存一个标记(不包含敏感信息),业务登录态仍以服务端为准。
|
|
|
|
|
|
NSString *userID = credential.user ?: @"";
|
|
|
|
|
|
if (userID.length > 0) {
|
|
|
|
|
|
[self.class keychainSave:kKBAppleUserIdentifierKey value:userID];
|
|
|
|
|
|
}
|
2025-10-30 14:29:11 +08:00
|
|
|
|
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)) {
|
2025-11-13 14:11:44 +08:00
|
|
|
|
// 优先用传入 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
|
2025-10-30 14:29:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#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
|