309 lines
14 KiB
Objective-C
309 lines
14 KiB
Objective-C
//
|
||
// KBLoginVM.m
|
||
//
|
||
|
||
#import "KBLoginVM.h"
|
||
#import "AppleSignInManager.h"
|
||
#import "KBNetworkManager.h"
|
||
#import "KBAuthManager.h"
|
||
#import "KBAPI.h"
|
||
#import "KBUser.h"
|
||
#import "KBMyVM.h"
|
||
|
||
@interface KBLoginVM ()
|
||
@property (atomic, strong, readwrite, nullable) KBUser *currentUser;
|
||
@property (nonatomic, strong) KBMyVM *myVM;
|
||
@end
|
||
|
||
@implementation KBAppUpdateInfo
|
||
@end
|
||
|
||
@implementation KBAppUpdateResponse
|
||
@end
|
||
|
||
@implementation KBLoginVM
|
||
|
||
+ (instancetype)shared {
|
||
static KBLoginVM *vm; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ vm = [KBLoginVM new]; });
|
||
return vm;
|
||
}
|
||
|
||
- (BOOL)isLoggedIn {
|
||
return [[KBAuthManager shared] isLoggedIn];
|
||
}
|
||
|
||
- (void)signInWithAppleFromViewController:(UIViewController *)presenter
|
||
completion:(KBLoginCompletion)completion {
|
||
// 调起 Apple 登录
|
||
[[AppleSignInManager shared] signInFromViewController:presenter completion:^(ASAuthorizationAppleIDCredential * _Nullable credential, NSError * _Nullable error) {
|
||
if (error) { if (completion) completion(NO, error); return; }
|
||
if (![credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
|
||
if (completion) completion(NO, [NSError errorWithDomain:@"KBLogin" code:-1 userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid login credential")}]);
|
||
return;
|
||
}
|
||
|
||
ASAuthorizationAppleIDCredential *cred = (ASAuthorizationAppleIDCredential *)credential;
|
||
// identityToken/authorizationCode 均可按后端要求传递;本项目后端使用 identityToken 映射为 code
|
||
NSString *identityToken = cred.identityToken ? [[NSString alloc] initWithData:cred.identityToken encoding:NSUTF8StringEncoding] : nil;
|
||
NSString *authorizationCode = cred.authorizationCode ? [[NSString alloc] initWithData:cred.authorizationCode encoding:NSUTF8StringEncoding] : nil;
|
||
|
||
NSMutableDictionary *params = [NSMutableDictionary dictionary];
|
||
if (identityToken.length) params[@"identityToken"] = identityToken;
|
||
if (authorizationCode.length) params[@"accessCode"] = authorizationCode; // 仅供后端需要时使用
|
||
if (cred.user.length) params[@"userID"] = cred.user; // 可选
|
||
// 如有本地缓存的性别(首次进入首页时选择),一并上传给后端
|
||
NSNumber *genderNumber = [self kb_localGenderParamIfAvailable];
|
||
if (genderNumber != nil) {
|
||
params[@"gender"] = genderNumber;
|
||
}
|
||
[KBHUD show];
|
||
// 向服务端发起校验
|
||
[[KBNetworkManager shared] POST:API_APPLE_LOGIN jsonBody:params headers:nil completion:^(NSDictionary * _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||
[KBHUD dismiss];
|
||
if (error) { if (completion) completion(NO, error); return; }
|
||
|
||
// 从返回 JSON 中提取 token(支持常见命名与层级 data/user)
|
||
// 先解析用户模型(使用 MJExtension)
|
||
NSDictionary *dict = jsonOrData[@"data"];
|
||
KBUser *user = [KBUser mj_objectWithKeyValues:dict];
|
||
self.currentUser = user;
|
||
if (user.token.length == 0) {
|
||
if (completion) completion(NO, [NSError errorWithDomain:@"KBLogin" code:-2 userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"No token returned")}]);
|
||
return;
|
||
}
|
||
[[KBUserSessionManager shared] handleLoginSuccessWithUser:user];
|
||
[self kb_syncKeyboardCharactersAfterLogin];
|
||
if (completion) completion(YES, nil);
|
||
// 保存登录态到共享钥匙串;供 App 与扩展共享
|
||
// BOOL ok = [[KBAuthManager shared] saveAccessToken:user.token
|
||
// refreshToken:nil
|
||
// expiryDate:nil
|
||
// userIdentifier:cred.user];
|
||
// if (completion) completion(ok, ok ? nil : [NSError errorWithDomain:@"KBLogin" code:-3 userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Failed to save login state")}]);
|
||
}];
|
||
}];
|
||
}
|
||
|
||
/// 邮箱登录
|
||
- (void)emailLoginEmail:(NSString *)email password:(NSString *)password WithCompletion:(KBLoginCompletion)completion;
|
||
{
|
||
[KBHUD show];
|
||
NSMutableDictionary *params = [NSMutableDictionary dictionary];
|
||
if (email.length) params[@"mail"] = email;
|
||
if (password.length) params[@"password"] = password;
|
||
// 如有本地缓存的性别(首次进入首页时选择),一并上传给后端
|
||
NSNumber *genderNumber = [self kb_localGenderParamIfAvailable];
|
||
if (genderNumber != nil) {
|
||
params[@"gender"] = genderNumber;
|
||
}
|
||
// 向服务端发起校验
|
||
[[KBNetworkManager shared] POST:API_EMAIL_LOGIN jsonBody:params headers:nil completion:^(NSDictionary * _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||
[KBHUD dismiss];
|
||
if (error) { if (completion) completion(NO, error); return; }
|
||
NSDictionary *dict = jsonOrData[@"data"];
|
||
KBUser *user = [KBUser mj_objectWithKeyValues:dict];
|
||
self.currentUser = user;
|
||
if (user.token.length == 0) {
|
||
if (completion) completion(NO, [NSError errorWithDomain:@"KBLogin" code:-2 userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"No token returned")}]);
|
||
return;
|
||
}
|
||
[[KBUserSessionManager shared] handleLoginSuccessWithUser:user];
|
||
[self kb_syncKeyboardCharactersAfterLogin];
|
||
if (completion) completion(YES, nil);
|
||
}];
|
||
}
|
||
|
||
/// 邮箱注册
|
||
- (void)emailRegisterParams:(NSDictionary *)params withCompletion:(KBRegisterCompletion)completion{
|
||
[KBHUD show];
|
||
// NSMutableDictionary *params = [NSMutableDictionary dictionary];
|
||
// if (mailAddress.length) params[@"mailAddress"] = mailAddress;
|
||
// if (password.length) params[@"password"] = password;
|
||
// if (passwordConfirm.length) params[@"passwordConfirm"] = passwordConfirm;
|
||
//
|
||
// // 如有本地缓存的性别(首次进入首页时选择),一并上传给后端
|
||
// NSNumber *genderNumber = [self kb_localGenderParamIfAvailable];
|
||
// if (genderNumber != nil) {
|
||
// params[@"gender"] = genderNumber;
|
||
// }
|
||
// 向服务端发起校验
|
||
[[KBNetworkManager shared] POST:API_EMAIL_REGISTER jsonBody:params headers:nil completion:^(NSDictionary * _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||
[KBHUD dismiss];
|
||
if (error) { if (completion) completion(NO, error); return; }
|
||
if (completion) completion(YES, nil);
|
||
}];
|
||
}
|
||
|
||
/// 发送验证码
|
||
- (void)sendVerifyMailWithEmail:(NSString *)email withCompletion:(KBVerifyMailCompletion)completion{
|
||
NSMutableDictionary *params = [NSMutableDictionary dictionary];
|
||
if (email.length) params[@"mailAddress"] = email;
|
||
[[KBNetworkManager shared] POST:API_SEND_EMAIL_VERIFYMAIL jsonBody:params headers:nil autoShowBusinessError:true completion:^(NSDictionary * _Nullable json, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||
|
||
}];
|
||
}
|
||
|
||
/// 验证验证码
|
||
- (void)verifyEMailCode:(NSString *)email verifyCode:(NSString *)verifyCode withCompletion:(KBVerifyMailCompletion)completion{
|
||
NSMutableDictionary *params = [NSMutableDictionary dictionary];
|
||
if (email.length){
|
||
params[@"mailAddress"] = email;
|
||
params[@"verifyCode"] = verifyCode;
|
||
|
||
}
|
||
[[KBNetworkManager shared] POST:API_VERIFY_EMAIL_CODE jsonBody:params headers:nil autoShowBusinessError:true completion:^(NSDictionary * _Nullable json, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||
if (error) { if (completion) completion(NO, error); return; }
|
||
if (completion) completion(YES, nil);
|
||
}];
|
||
|
||
}
|
||
/// 重置密码
|
||
- (void)resetPassWord:(NSString *)email password:(NSString *)password confirmPassword:(NSString *)confirmPassword withCompletion:(KBVerifyMailCompletion)completion;{
|
||
NSMutableDictionary *params = [NSMutableDictionary dictionary];
|
||
params[@"mailAddress"] = email ? email : @"";
|
||
params[@"password"] = password ? password : @"";
|
||
params[@"confirmPassword"] = confirmPassword;
|
||
|
||
[[KBNetworkManager shared] POST:API_RESET_PWD jsonBody:params headers:nil autoShowBusinessError:true completion:^(NSDictionary * _Nullable json, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||
if (error) { if (completion) completion(NO, error); return; }
|
||
if (completion) completion(YES, nil);
|
||
}];
|
||
}
|
||
|
||
/// 检查更新
|
||
- (void)checkAppUpdateWithCompletion:(KBAppUpdateCompletion)completion {
|
||
NSDictionary *params = [self kb_appUpdateParams];
|
||
[[KBNetworkManager shared] POST:API_APP_CHECK_UPDATE jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary * _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||
if (error) { if (completion) completion(nil, error); return; }
|
||
if (![jsonOrData isKindOfClass:NSDictionary.class]) { if (completion) completion(nil, nil); return; }
|
||
KBAppUpdateResponse *resp = [KBAppUpdateResponse mj_objectWithKeyValues:jsonOrData];
|
||
if (resp.code != 0) { if (completion) completion(nil, nil); return; }
|
||
if (completion) completion(resp.data, nil);
|
||
}];
|
||
}
|
||
|
||
#pragma mark - Private
|
||
|
||
- (void)kb_syncKeyboardCharactersAfterLogin {
|
||
if (!self.myVM) {
|
||
self.myVM = [[KBMyVM alloc] init];
|
||
}
|
||
/// 防止token写入过慢导致请求head没有token
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||
[self.myVM fetchCharacterListByUserWithCompletion:^(NSArray<KBCharacter *> * _Nonnull characterArray, NSError * _Nullable error) {
|
||
if (error) {
|
||
KBLOG(@"[Login] Failed to sync keyboard characters after login: %@", error);
|
||
} else {
|
||
KBLOG(@"[Login] Synced %tu keyboard characters after login", characterArray.count);
|
||
}
|
||
}];
|
||
});
|
||
|
||
}
|
||
|
||
#pragma mark - Helpers
|
||
|
||
// 宽松解析:token / access_token / accessToken,支持顶层或 data/user 层
|
||
+ (NSString *)tokenFromResponseObject:(id)obj {
|
||
if (![obj isKindOfClass:[NSDictionary class]]) return nil;
|
||
NSDictionary *dict = (NSDictionary *)obj;
|
||
|
||
NSString *(^pick)(NSDictionary *) = ^NSString *(NSDictionary *d) {
|
||
NSArray *keys = @[ @"token", @"access_token", @"accessToken" ];
|
||
for (NSString *k in keys) {
|
||
id v = d[k]; if ([v isKindOfClass:NSString.class] && ((NSString *)v).length > 0) return v;
|
||
}
|
||
return nil;
|
||
};
|
||
|
||
NSString *t = pick(dict);
|
||
if (t.length) return t;
|
||
|
||
id data = dict[@"data"]; if ([data isKindOfClass:NSDictionary.class]) { t = pick(data); if (t.length) return t; }
|
||
id user = dict[@"user"]; if ([user isKindOfClass:NSDictionary.class]) { t = pick(user); if (t.length) return t; }
|
||
|
||
// 扩展:允许后端将 token 放在 data.session.token
|
||
NSDictionary *d2 = dict[@"data"]; if ([d2 isKindOfClass:NSDictionary.class]) {
|
||
NSDictionary *session = d2[@"session"]; if ([session isKindOfClass:NSDictionary.class]) { t = pick(session); if (t.length) return t; }
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
/// 读取本地缓存的性别枚举值,用于登录接口上传到后端。
|
||
/// - 用户在首次进入首页的性别选择页或个人资料页中选择后,会持久化到 NSUserDefaults。
|
||
- (nullable NSNumber *)kb_localGenderParamIfAvailable {
|
||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||
// 只有在用户真正看过性别选择页后,才认为本地的性别有效
|
||
BOOL hasShownSexVC = [ud boolForKey:KBSexSelectShownKey];
|
||
if (!hasShownSexVC) {
|
||
return nil;
|
||
}
|
||
|
||
NSInteger value = [ud integerForKey:KBSexSelectedGenderKey];
|
||
if (value < UserSexMan || value > UserSexTwoSex) {
|
||
return nil;
|
||
}
|
||
return @(value);
|
||
}
|
||
|
||
- (NSDictionary *)kb_appUpdateParams {
|
||
NSMutableDictionary *params = [NSMutableDictionary dictionary];
|
||
params[@"appId"] = @"main";
|
||
params[@"platform"] = @"ios";
|
||
params[@"channel"] = @"AppStore";
|
||
NSString *shortVersion = [self kb_bundleShortVersion];
|
||
if (shortVersion.length) {
|
||
params[@"clientVersionName"] = shortVersion;
|
||
}
|
||
NSString *buildNumber = [self kb_bundleBuildNumber];
|
||
if (buildNumber.length) {
|
||
params[@"buildNumber"] = buildNumber;
|
||
}
|
||
NSNumber *versionCode = [self kb_versionCodeFromBuildNumber:buildNumber shortVersion:shortVersion];
|
||
// if (versionCode) {
|
||
params[@"clientVersionCode"] = @1;
|
||
// }
|
||
return params;
|
||
}
|
||
|
||
- (nullable NSString *)kb_bundleShortVersion {
|
||
id obj = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
||
if ([obj isKindOfClass:NSString.class]) { return (NSString *)obj; }
|
||
if ([obj respondsToSelector:@selector(stringValue)]) { return [obj stringValue]; }
|
||
return nil;
|
||
}
|
||
|
||
- (nullable NSString *)kb_bundleBuildNumber {
|
||
id obj = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
||
if ([obj isKindOfClass:NSString.class]) { return (NSString *)obj; }
|
||
if ([obj respondsToSelector:@selector(stringValue)]) { return [obj stringValue]; }
|
||
return nil;
|
||
}
|
||
|
||
- (nullable NSNumber *)kb_versionCodeFromBuildNumber:(nullable NSString *)buildNumber
|
||
shortVersion:(nullable NSString *)shortVersion {
|
||
NSString *digits = [self kb_digitsOnlyStringFromString:buildNumber];
|
||
if (digits.length == 0) {
|
||
digits = [self kb_digitsOnlyStringFromString:shortVersion];
|
||
}
|
||
if (digits.length == 0) { return nil; }
|
||
long long value = digits.longLongValue;
|
||
if (value <= 0) { return nil; }
|
||
return @(value);
|
||
}
|
||
|
||
- (nullable NSString *)kb_digitsOnlyStringFromString:(nullable NSString *)string {
|
||
if (![string isKindOfClass:NSString.class] || string.length == 0) { return nil; }
|
||
NSCharacterSet *set = [NSCharacterSet decimalDigitCharacterSet];
|
||
NSMutableString *digits = [NSMutableString string];
|
||
for (NSUInteger i = 0; i < string.length; i++) {
|
||
unichar c = [string characterAtIndex:i];
|
||
if ([set characterIsMember:c]) {
|
||
[digits appendFormat:@"%C", c];
|
||
}
|
||
}
|
||
return digits;
|
||
}
|
||
|
||
@end
|