3
This commit is contained in:
@@ -7,10 +7,9 @@
|
||||
#ifndef KBAPI_h
|
||||
#define KBAPI_h
|
||||
|
||||
// 用户与认证
|
||||
#ifndef KB_API_USER_APPLE_LOGIN
|
||||
|
||||
// 兼容旧命名(如有使用 API_APPLE_LOGIN 的位置,映射到统一命名)
|
||||
#define API_APPLE_LOGIN @"/user/appleLogin" // Apple 登录
|
||||
#endif
|
||||
|
||||
// 应用配置
|
||||
#ifndef KB_API_APP_CONFIG
|
||||
@@ -18,4 +17,3 @@
|
||||
#endif
|
||||
|
||||
#endif /* KBAPI_h */
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
04122F5D2EC5E5A900EF7AB3 /* KBLoginVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F5B2EC5E5A900EF7AB3 /* KBLoginVM.m */; };
|
||||
04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F612EC5F41D00EF7AB3 /* KBUser.m */; };
|
||||
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; };
|
||||
0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; };
|
||||
0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
|
||||
@@ -177,6 +179,10 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
04122F592EC5D40000EF7AB3 /* KBAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAPI.h; sourceTree = "<group>"; };
|
||||
04122F5A2EC5E5A900EF7AB3 /* KBLoginVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLoginVM.h; sourceTree = "<group>"; };
|
||||
04122F5B2EC5E5A900EF7AB3 /* KBLoginVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLoginVM.m; sourceTree = "<group>"; };
|
||||
04122F602EC5F41D00EF7AB3 /* KBUser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBUser.h; sourceTree = "<group>"; };
|
||||
04122F612EC5F41D00EF7AB3 /* KBUser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBUser.m; sourceTree = "<group>"; };
|
||||
0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinCenterVC.h; sourceTree = "<group>"; };
|
||||
0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = "<group>"; };
|
||||
0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = "<group>"; };
|
||||
@@ -453,6 +459,15 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
04122F5C2EC5E5A900EF7AB3 /* VM */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04122F5A2EC5E5A900EF7AB3 /* KBLoginVM.h */,
|
||||
04122F5B2EC5E5A900EF7AB3 /* KBLoginVM.m */,
|
||||
);
|
||||
path = VM;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0477BD942EBAFF4E0055D639 /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1099,7 +1114,9 @@
|
||||
04FC95EA2EB33611007BD342 /* M */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
04122F602EC5F41D00EF7AB3 /* KBUser.h */,
|
||||
04122F612EC5F41D00EF7AB3 /* KBUser.m */,
|
||||
);
|
||||
path = M;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -1126,6 +1143,7 @@
|
||||
04FC95ED2EB33611007BD342 /* Login */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04122F5C2EC5E5A900EF7AB3 /* VM */,
|
||||
04FC95EA2EB33611007BD342 /* M */,
|
||||
04FC95EB2EB33611007BD342 /* V */,
|
||||
04FC95EC2EB33611007BD342 /* VC */,
|
||||
@@ -1459,6 +1477,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */,
|
||||
049FB31D2EC21BCD00FAB05D /* KBMyKeyboardCell.m in Sources */,
|
||||
048909F62EC0AAAA00FABA60 /* KBCategoryTitleCell.m in Sources */,
|
||||
048909F72EC0AAAA00FABA60 /* KBCategoryTitleView.m in Sources */,
|
||||
@@ -1539,6 +1558,7 @@
|
||||
A1B2C4002EB4A0A100000004 /* KBAuthManager.m in Sources */,
|
||||
047C65532EBCBAC60035E841 /* KBCommunityVC.m in Sources */,
|
||||
A1B2C4212EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */,
|
||||
04122F5D2EC5E5A900EF7AB3 /* KBLoginVM.m in Sources */,
|
||||
0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */,
|
||||
048908E32EBF760000FABA60 /* MySkinCell.m in Sources */,
|
||||
04890B122EC2F00000FABA60 /* KBMyHeaderView.m in Sources */,
|
||||
|
||||
@@ -135,52 +135,14 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.loveKey.nyx.CustomK
|
||||
__weak typeof(pop) weakPop = pop;
|
||||
view.appleLoginHandler = ^{
|
||||
[weakPop dismiss];
|
||||
[[AppleSignInManager shared] signInFromViewController:KB_CURRENT_NAV completion:^(ASAuthorizationAppleIDCredential * _Nullable credential, NSError * _Nullable error) {
|
||||
NSLog(@"=====");
|
||||
// 成功回调:仅处理 AppleID 凭证
|
||||
if (![credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
|
||||
return;
|
||||
}
|
||||
ASAuthorizationAppleIDCredential *cred = (ASAuthorizationAppleIDCredential *)credential;
|
||||
|
||||
// identityToken 与 authorizationCode 需要发给后端校验
|
||||
NSString *tokenString = nil;
|
||||
if (cred.identityToken) {
|
||||
tokenString = [[NSString alloc] initWithData:cred.identityToken encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
NSString *authorizationCodeString = nil;
|
||||
if (cred.authorizationCode) {
|
||||
authorizationCodeString = [[NSString alloc] initWithData:cred.authorizationCode encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
// fullName 仅在首次授权时可能返回
|
||||
NSString *fullName = nil;
|
||||
if (cred.fullName.givenName.length || cred.fullName.familyName.length) {
|
||||
NSString *given = cred.fullName.givenName ?: @"";
|
||||
NSString *family = cred.fullName.familyName ?: @"";
|
||||
fullName = [[NSString stringWithFormat:@"%@ %@", given, family] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
}
|
||||
|
||||
// 保存 user 标识,后续可以查询撤销状态或和账号体系绑定(生产建议存钥匙串)
|
||||
NSString *userID = cred.user;
|
||||
// [self _persistAppleUserIdentifier:userID];
|
||||
|
||||
// NSDictionary *params = @{
|
||||
// @"userID": userID ?: @"",
|
||||
// @"accessCode": authorizationCodeString ?: @"",
|
||||
// // 可选字段:若后端不接受,请删除下列键
|
||||
// @"identityToken": tokenString ?: @"",
|
||||
// @"fullName": fullName ?: @"",
|
||||
// @"state": cred.state ?: @""
|
||||
// };
|
||||
NSDictionary *params = @{
|
||||
@"code": tokenString ?: @"",
|
||||
};
|
||||
[[KBNetworkManager shared] POST:API_APPLE_LOGIN jsonBody:params headers:nil completion:^(id _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
NSLog(@"=====");
|
||||
}];
|
||||
|
||||
NSLog(@"====");
|
||||
// 交给 VM 统一处理 Apple 登录 + 服务端登录
|
||||
[[KBLoginVM shared] signInWithAppleFromViewController:KB_CURRENT_NAV completion:^(BOOL success, NSError * _Nullable error) {
|
||||
if (success) {
|
||||
[KBHUD showInfo:@"登录成功"];
|
||||
} else {
|
||||
NSString *msg = error.localizedDescription ?: @"登录失败";
|
||||
[KBHUD showInfo:msg];
|
||||
}
|
||||
}];
|
||||
};
|
||||
view.closeHandler = ^{ [weakPop dismiss]; };
|
||||
|
||||
34
keyBoard/Class/Login/M/KBUser.h
Normal file
34
keyBoard/Class/Login/M/KBUser.h
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// KBUser.h
|
||||
// 登录模块-用户模型(MJExtension 解析)
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBUser : NSObject
|
||||
|
||||
// 标识
|
||||
@property (nonatomic, copy, nullable) NSString *userId; // id/user_id/uid
|
||||
@property (nonatomic, copy, nullable) NSString *appleUserId; // 用 Apple 登录返回的 userID(可选)
|
||||
|
||||
// 基本信息
|
||||
@property (nonatomic, copy, nullable) NSString *nickname;
|
||||
@property (nonatomic, copy, nullable) NSString *avatar; // 头像 URL
|
||||
@property (nonatomic, copy, nullable) NSString *gender; // 性别(后端可能返回 string/int,统一转字符串存)
|
||||
@property (nonatomic, copy, nullable) NSString *mobile;
|
||||
@property (nonatomic, copy, nullable) NSString *email;
|
||||
|
||||
// 会话信息
|
||||
@property (nonatomic, copy, nullable) NSString *token; // token/access_token/accessToken
|
||||
@property (nonatomic, copy, nullable) NSString *refreshToken; // refresh_token/refreshToken
|
||||
@property (nonatomic, strong, nullable) NSDate *expiryDate; // 若后端返回过期时间,转为日期
|
||||
|
||||
/// 从后端返回(可能顶层或 data/user 嵌套)中解析用户模型。内部使用 MJExtension。
|
||||
+ (instancetype)userFromResponseObject:(id)jsonObject;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
97
keyBoard/Class/Login/M/KBUser.m
Normal file
97
keyBoard/Class/Login/M/KBUser.m
Normal file
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// KBUser.m
|
||||
//
|
||||
|
||||
#import "KBUser.h"
|
||||
#import <MJExtension/MJExtension.h>
|
||||
|
||||
@implementation KBUser
|
||||
|
||||
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
|
||||
return @{
|
||||
@"userId": @[@"id", @"user_id", @"uid"],
|
||||
@"appleUserId": @[@"appleUserId", @"apple_user_id", @"apple_userid", @"appleUserID"],
|
||||
@"nickname": @[@"nickname", @"nick", @"name"],
|
||||
@"avatar": @[@"avatar", @"avatar_url", @"head", @"headimg"],
|
||||
@"gender": @[@"gender", @"sex"],
|
||||
@"mobile": @[@"mobile", @"phone"],
|
||||
@"email": @[@"email"],
|
||||
@"token": @[@"token", @"access_token", @"accessToken"],
|
||||
@"refreshToken": @[@"refresh_token", @"refreshToken"],
|
||||
@"expiryDate": @[@"expire_at", @"expireAt", @"expires_at", @"expiresAt", @"expired_at"],
|
||||
};
|
||||
}
|
||||
|
||||
// 将可能是字符串/时间戳(秒/毫秒)的过期时间,转为 NSDate
|
||||
- (void)setExpiryDate:(NSDate *)expiryDate { _expiryDate = expiryDate; }
|
||||
|
||||
+ (instancetype)userFromResponseObject:(id)jsonObject {
|
||||
if (!jsonObject) return nil;
|
||||
NSDictionary *dict = nil;
|
||||
if ([jsonObject isKindOfClass:NSDictionary.class]) {
|
||||
dict = (NSDictionary *)jsonObject;
|
||||
} else if ([jsonObject isKindOfClass:NSData.class]) {
|
||||
// JSON data -> dict
|
||||
id obj = [NSJSONSerialization JSONObjectWithData:(NSData *)jsonObject options:0 error:NULL];
|
||||
if ([obj isKindOfClass:NSDictionary.class]) dict = obj;
|
||||
}
|
||||
if (!dict) return nil;
|
||||
|
||||
// 兼容多种后端包装:优先 data.user,其次 data,本身就是用户
|
||||
NSDictionary *candidate = nil;
|
||||
id data = dict[@"data"]; if ([data isKindOfClass:NSDictionary.class]) { candidate = data; }
|
||||
id user = [candidate objectForKey:@"user"]; if (![user isKindOfClass:NSDictionary.class]) { user = dict[@"user"]; }
|
||||
NSDictionary *userDict = ([user isKindOfClass:NSDictionary.class]) ? (NSDictionary *)user : (candidate ?: dict);
|
||||
|
||||
KBUser *u = [KBUser mj_objectWithKeyValues:userDict];
|
||||
|
||||
// 额外兼容:若 token 不在用户对象里,尝试从上层提取到模型上
|
||||
if (u.token.length == 0) {
|
||||
NSString *t = [self pickTokenFromDictionary:dict];
|
||||
if (t.length) u.token = t;
|
||||
}
|
||||
|
||||
// 过期时间字段可能是字符串或时间戳,做宽松转换
|
||||
id exp = userDict[@"expire_at"] ?: userDict[@"expireAt"] ?: userDict[@"expires_at"] ?: userDict[@"expiresAt"] ?: userDict[@"expired_at"];
|
||||
if ([exp isKindOfClass:NSNumber.class]) {
|
||||
// 兼容秒/毫秒(> 10^11 视为毫秒)
|
||||
NSTimeInterval ts = [(NSNumber *)exp doubleValue];
|
||||
if (ts > 1e11) ts = ts / 1000.0;
|
||||
u.expiryDate = [NSDate dateWithTimeIntervalSince1970:ts];
|
||||
} else if ([exp isKindOfClass:NSString.class]) {
|
||||
// 尝试 ISO8601
|
||||
NSDateFormatter *fmt = [NSDateFormatter new];
|
||||
fmt.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
|
||||
fmt.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZ";
|
||||
NSDate *d = [fmt dateFromString:(NSString *)exp];
|
||||
if (!d) {
|
||||
// 尝试纯秒
|
||||
NSTimeInterval ts = [(NSString *)exp doubleValue];
|
||||
if (ts > 0) d = [NSDate dateWithTimeIntervalSince1970:ts];
|
||||
}
|
||||
if (d) u.expiryDate = d;
|
||||
}
|
||||
|
||||
return u;
|
||||
}
|
||||
|
||||
+ (NSString *)pickTokenFromDictionary:(NSDictionary *)dict {
|
||||
if (![dict isKindOfClass:NSDictionary.class]) return nil;
|
||||
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; }
|
||||
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;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
32
keyBoard/Class/Login/VM/KBLoginVM.h
Normal file
32
keyBoard/Class/Login/VM/KBLoginVM.h
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// KBLoginVM.h
|
||||
// 登录相关的 ViewModel:封装 Apple 登录与服务端校验、登录态落盘。
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class KBUser;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 登录完成回调
|
||||
typedef void(^KBLoginCompletion)(BOOL success, NSError * _Nullable error);
|
||||
|
||||
@interface KBLoginVM : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 最近一次登录/拉取到的用户信息(仅内存缓存),可选
|
||||
@property (atomic, strong, readonly, nullable) KBUser *currentUser; // 最近一次解析得到的用户
|
||||
|
||||
/// 调起 Apple 登录并在服务端完成校验;成功后会将 token 写入共享钥匙串(KBAuthManager),作为“已登录”的判断依据。
|
||||
- (void)signInWithAppleFromViewController:(UIViewController *)presenter
|
||||
completion:(KBLoginCompletion)completion;
|
||||
|
||||
/// 是否已登录:由 KBAuthManager 判断(是否存在有效 token)
|
||||
- (BOOL)isLoggedIn;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
99
keyBoard/Class/Login/VM/KBLoginVM.m
Normal file
99
keyBoard/Class/Login/VM/KBLoginVM.m
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// KBLoginVM.m
|
||||
//
|
||||
|
||||
#import "KBLoginVM.h"
|
||||
#import "AppleSignInManager.h"
|
||||
#import "KBNetworkManager.h"
|
||||
#import "KBAuthManager.h"
|
||||
#import "KBAPI.h"
|
||||
#import "KBUser.h"
|
||||
|
||||
@interface KBLoginVM ()
|
||||
@property (atomic, strong, readwrite, nullable) KBUser *currentUser;
|
||||
@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: @"无效的登录凭证"}]);
|
||||
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[@"code"] = identityToken;
|
||||
if (authorizationCode.length) params[@"accessCode"] = authorizationCode; // 仅供后端需要时使用
|
||||
if (cred.user.length) params[@"userID"] = cred.user; // 可选
|
||||
|
||||
// 向服务端发起校验
|
||||
[[KBNetworkManager shared] POST:API_APPLE_LOGIN jsonBody:params headers:nil completion:^(id _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
if (error) { if (completion) completion(NO, error); return; }
|
||||
|
||||
// 从返回 JSON 中提取 token(支持常见命名与层级 data/user)
|
||||
// 先解析用户模型(使用 MJExtension)
|
||||
KBUser *user = [KBUser userFromResponseObject:jsonOrData];
|
||||
self.currentUser = user;
|
||||
NSString *token = user.token ?: [self.class tokenFromResponseObject:jsonOrData];
|
||||
if (token.length == 0) {
|
||||
if (completion) completion(NO, [NSError errorWithDomain:@"KBLogin" code:-2 userInfo:@{NSLocalizedDescriptionKey: @"未返回 token"}]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存登录态到共享钥匙串;供 App 与扩展共享
|
||||
BOOL ok = [[KBAuthManager shared] saveAccessToken:token
|
||||
refreshToken:nil
|
||||
expiryDate:nil
|
||||
userIdentifier:cred.user];
|
||||
if (completion) completion(ok, ok ? nil : [NSError errorWithDomain:@"KBLogin" code:-3 userInfo:@{NSLocalizedDescriptionKey: @"保存登录态失败"}]);
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -15,7 +15,12 @@
|
||||
// 顶部左侧标题与点缀
|
||||
@property (nonatomic, strong) UILabel *titleLabel; // "Points\nMall"
|
||||
@property (nonatomic, strong) UIImageView *yellowStarView; // 左侧黄色装饰星:shop_yellowxx_icon
|
||||
@property (nonatomic, strong) UIImageView *yellowStarView2; // 左侧黄色装饰星:shop_yellowxx_icon
|
||||
|
||||
@property (nonatomic, strong) UIImageView *greenStarView; // 右上角绿色装饰星:shop_greenxx_icon
|
||||
@property (nonatomic, strong) UIImageView *greenStarView2; // 右上角绿色装饰星:shop_greenxx_icon
|
||||
@property (nonatomic, strong) UIImageView *greenStarView3; // 右上角绿色装饰星:shop_greenxx_icon
|
||||
|
||||
|
||||
// 右侧大金币
|
||||
@property (nonatomic, strong) UIImageView *bigCoinView; // shop_headbigBg_icon
|
||||
@@ -38,9 +43,12 @@
|
||||
// 组装视图层级
|
||||
[self addSubview:self.containerView];
|
||||
[self.containerView addSubview:self.titleLabel];
|
||||
[self.containerView addSubview:self.yellowStarView];
|
||||
[self.containerView addSubview:self.greenStarView];
|
||||
[self.containerView addSubview:self.bigCoinView];
|
||||
[self.containerView addSubview:self.yellowStarView];
|
||||
[self.containerView addSubview:self.yellowStarView2];
|
||||
[self.containerView addSubview:self.greenStarView];
|
||||
[self.containerView addSubview:self.greenStarView2];
|
||||
[self.containerView addSubview:self.greenStarView3];
|
||||
[self.containerView addSubview:self.infoCard];
|
||||
[self.infoCard addSubview:self.myPointsLabel];
|
||||
[self.infoCard addSubview:self.smallCoinView];
|
||||
@@ -57,21 +65,36 @@
|
||||
|
||||
// 左侧标题
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.left.equalTo(self.containerView).offset(0);
|
||||
make.bottom.equalTo(self.containerView).offset(0);
|
||||
make.left.equalTo(self.containerView).offset(32);
|
||||
make.top.equalTo(self.containerView).offset(KB_NAV_TOTAL_HEIGHT + 20);
|
||||
}];
|
||||
|
||||
// 左侧黄色装饰星
|
||||
[self.yellowStarView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerY.equalTo(self.titleLabel.mas_centerY).offset(6);
|
||||
make.left.equalTo(self.containerView).offset(6);
|
||||
make.width.height.mas_equalTo(18);
|
||||
}];
|
||||
[self.yellowStarView2 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.bottom.equalTo(self.titleLabel.mas_top).offset(6);
|
||||
make.left.equalTo(self.titleLabel.mas_right).offset(6);
|
||||
make.width.height.mas_equalTo(16);
|
||||
}];
|
||||
|
||||
// 右上角绿色装饰星
|
||||
[self.greenStarView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.containerView).offset(10);
|
||||
make.right.equalTo(self.containerView).offset(-6);
|
||||
make.top.equalTo(self.yellowStarView2).offset(0);
|
||||
make.left.equalTo(self.yellowStarView2.mas_right).offset(62);
|
||||
make.width.height.mas_equalTo(20);
|
||||
}];
|
||||
[self.greenStarView2 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.greenStarView.mas_bottom).offset(10);
|
||||
make.right.equalTo(self.containerView).offset(-18);
|
||||
make.width.height.mas_equalTo(14);
|
||||
}];
|
||||
[self.greenStarView3 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.greenStarView2.mas_bottom).offset(20);
|
||||
make.right.equalTo(self.containerView).offset(0);
|
||||
make.width.height.mas_equalTo(14);
|
||||
}];
|
||||
|
||||
@@ -91,25 +114,25 @@
|
||||
|
||||
[self.myPointsLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.infoCard).offset(16);
|
||||
make.top.equalTo(self.infoCard).offset(16);
|
||||
make.bottom.equalTo(self.smallCoinView.mas_top).offset(-16);
|
||||
}];
|
||||
|
||||
[self.smallCoinView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.infoCard).offset(16);
|
||||
make.top.equalTo(self.myPointsLabel.mas_bottom).offset(12);
|
||||
make.width.height.mas_equalTo(36);
|
||||
make.centerY.equalTo(self.amountLabel);
|
||||
make.width.height.mas_equalTo(38);
|
||||
}];
|
||||
|
||||
[self.amountLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.smallCoinView.mas_right).offset(10);
|
||||
make.centerY.equalTo(self.smallCoinView);
|
||||
make.centerY.equalTo(self.rechargeButton);
|
||||
}];
|
||||
|
||||
[self.rechargeButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerY.equalTo(self.amountLabel);
|
||||
make.right.equalTo(self.infoCard).offset(-16);
|
||||
make.width.mas_equalTo(120);
|
||||
make.height.mas_equalTo(40);
|
||||
make.bottom.equalTo(self.infoCard).offset(-32);
|
||||
make.right.equalTo(self.infoCard).offset(-15);
|
||||
make.width.mas_equalTo(114);
|
||||
make.height.mas_equalTo(42);
|
||||
}];
|
||||
}
|
||||
return self;
|
||||
@@ -180,6 +203,13 @@
|
||||
}
|
||||
return _yellowStarView;
|
||||
}
|
||||
- (UIImageView *)yellowStarView2 {
|
||||
if (!_yellowStarView2) {
|
||||
_yellowStarView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"shop_yellowxx_icon"]];
|
||||
_yellowStarView2.contentMode = UIViewContentModeScaleAspectFit;
|
||||
}
|
||||
return _yellowStarView2;
|
||||
}
|
||||
|
||||
- (UIImageView *)greenStarView {
|
||||
if (!_greenStarView) {
|
||||
@@ -188,6 +218,20 @@
|
||||
}
|
||||
return _greenStarView;
|
||||
}
|
||||
- (UIImageView *)greenStarView2 {
|
||||
if (!_greenStarView2) {
|
||||
_greenStarView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"shop_greenxx_icon"]];
|
||||
_greenStarView2.contentMode = UIViewContentModeScaleAspectFit;
|
||||
}
|
||||
return _greenStarView2;
|
||||
}
|
||||
- (UIImageView *)greenStarView3 {
|
||||
if (!_greenStarView3) {
|
||||
_greenStarView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"shop_greenxx_icon"]];
|
||||
_greenStarView3.contentMode = UIViewContentModeScaleAspectFit;
|
||||
}
|
||||
return _greenStarView3;
|
||||
}
|
||||
|
||||
- (UIImageView *)bigCoinView {
|
||||
if (!_bigCoinView) {
|
||||
@@ -208,8 +252,8 @@
|
||||
if (!_myPointsLabel) {
|
||||
_myPointsLabel = [UILabel new];
|
||||
_myPointsLabel.text = @"My Points";
|
||||
_myPointsLabel.textColor = [UIColor colorWithHex:0x1B1F1A];
|
||||
_myPointsLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
|
||||
_myPointsLabel.textColor = [UIColor colorWithHex:KBBlackValue];
|
||||
_myPointsLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
|
||||
}
|
||||
return _myPointsLabel;
|
||||
}
|
||||
@@ -225,11 +269,11 @@
|
||||
- (UILabel *)amountLabel {
|
||||
if (!_amountLabel) {
|
||||
_amountLabel = [UILabel new];
|
||||
_amountLabel.text = @"88.00"; // 默认文案
|
||||
_amountLabel.text = @"88.00";
|
||||
_amountLabel.textColor = [UIColor colorWithHex:KBColorValue];
|
||||
_amountLabel.font = [UIFont systemFontOfSize:38 weight:UIFontWeightBold];
|
||||
_amountLabel.font = [UIFont systemFontOfSize:40 weight:UIFontWeightBold];
|
||||
_amountLabel.adjustsFontSizeToFitWidth = YES;
|
||||
_amountLabel.minimumScaleFactor = 0.7;
|
||||
// _amountLabel.minimumScaleFactor = 0.7;
|
||||
}
|
||||
return _amountLabel;
|
||||
}
|
||||
@@ -238,25 +282,28 @@
|
||||
if (!_rechargeButton) {
|
||||
_rechargeButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_rechargeButton setTitle:@"Recharge" forState:UIControlStateNormal];
|
||||
[_rechargeButton setTitleColor:[UIColor colorWithHex:0x1B1F1A] forState:UIControlStateNormal];
|
||||
_rechargeButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
|
||||
// 切图背景图(可拉伸)
|
||||
[_rechargeButton setTitleColor:[UIColor colorWithHex:KBBlackValue] forState:UIControlStateNormal];
|
||||
_rechargeButton.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
|
||||
UIImage *bg = [UIImage imageNamed:@"recharge_btn_bg"];
|
||||
if (bg) {
|
||||
CGFloat cap = 20; // 圆角拉伸区域
|
||||
bg = [bg resizableImageWithCapInsets:UIEdgeInsetsMake(cap, cap, cap, cap) resizingMode:UIImageResizingModeStretch];
|
||||
[_rechargeButton setBackgroundImage:bg forState:UIControlStateNormal];
|
||||
} else {
|
||||
// 兜底:用绿色渐变生成
|
||||
UIImage *fallback = [UIImage kb_gradientImageWithColors:@[[UIColor colorWithHex:0xA6F6E9], [UIColor colorWithHex:0xD6FFF4]]
|
||||
size:CGSizeMake(120, 40)
|
||||
direction:KBGradientDirectionLeftToRight];
|
||||
[_rechargeButton setBackgroundImage:fallback forState:UIControlStateNormal];
|
||||
}
|
||||
|
||||
_rechargeButton.layer.cornerRadius = 20;
|
||||
_rechargeButton.layer.masksToBounds = YES;
|
||||
[_rechargeButton setBackgroundImage:bg forState:UIControlStateNormal];
|
||||
|
||||
//
|
||||
// // 切图背景图(可拉伸)
|
||||
// UIImage *bg = [UIImage imageNamed:@"recharge_btn_bg"];
|
||||
// if (bg) {
|
||||
// CGFloat cap = 20; // 圆角拉伸区域
|
||||
// bg = [bg resizableImageWithCapInsets:UIEdgeInsetsMake(cap, cap, cap, cap) resizingMode:UIImageResizingModeStretch];
|
||||
// [_rechargeButton setBackgroundImage:bg forState:UIControlStateNormal];
|
||||
// } else {
|
||||
// // 兜底:用绿色渐变生成
|
||||
// UIImage *fallback = [UIImage kb_gradientImageWithColors:@[[UIColor colorWithHex:0xA6F6E9], [UIColor colorWithHex:0xD6FFF4]]
|
||||
// size:CGSizeMake(120, 40)
|
||||
// direction:KBGradientDirectionLeftToRight];
|
||||
// [_rechargeButton setBackgroundImage:fallback forState:UIControlStateNormal];
|
||||
// }
|
||||
//
|
||||
// _rechargeButton.layer.cornerRadius = 20;
|
||||
// _rechargeButton.layer.masksToBounds = YES;
|
||||
[_rechargeButton addTarget:self action:@selector(onRechargeTappedAction) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _rechargeButton;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
// 公共配置
|
||||
#import "KBConfig.h"
|
||||
#import "KBAPI.h" // 接口路径宏(统一管理)
|
||||
#import "KBLoginVM.h" // 登录 VM(Apple 登录复用)
|
||||
|
||||
/// 系统
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
Reference in New Issue
Block a user