// // 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 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); }]; } #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)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.myVM fetchCharacterListByUserWithCompletion:^(NSArray * _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); } @end