Files
keyboard/keyBoard/Class/Manager/AppleSignInManager.m
2025-10-30 14:29:11 +08:00

140 lines
5.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 (@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);
}
}
#pragma mark - 授权回调 (ASAuthorizationControllerDelegate)
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
if (@available(iOS 13.0, *)) {
ASAuthorizationAppleIDCredential *credential = authorization.credential;
// 持久化保存 userIdentifier便于后续校验凭证状态
NSString *userID = credential.user;
[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)) {
return self.presentingVC.view.window ?: UIApplication.sharedApplication.keyWindow;
}
#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