// // XSNetwork.m // XenonSDK // // Created by SAGESSE on 2019/5/28. // Copyright © 2019 SAGESSE. All rights reserved. // #import "XenonSDK.h" //#import "JSONModel.h" #import #import "XSTracker.h" #import "XSUtils.h" //#import "MBProgressHUD.h" #import #if SDK_HAS_IAP_PAYMENT #import "IAPAgnet.h" #import "KDIAPManager.h" #endif @interface XSNetwork () @end @implementation XSNetwork dispatch_semaphore_t sdk_net_semaphore; dispatch_queue_t sdk_net_waiting; NSInteger sdk_net_count; NSString* sdk_net_baseURL; MBProgressHUD* sdk_net_hud; + (NSString*)baseURL { return sdk_net_baseURL; } + (void)setBaseURL:(NSString *)baseURL { sdk_net_baseURL = baseURL; } + (NSError*)errorWithCode:(NSInteger)code message:(NSString*)message { if (message.length == 0) { message = @""; } return [NSError errorWithDomain:@"XSNetwork" code:code userInfo:@{NSLocalizedDescriptionKey:message}]; } + (void)showHudLoading { dispatch_async(dispatch_get_main_queue(), ^{ [sdk_net_hud removeFromSuperview]; sdk_net_hud = [MBProgressHUD showHUDAddedTo:UIApplication.sharedApplication.keyWindow animated:YES]; sdk_net_hud.label.text = nil; sdk_net_hud.removeFromSuperViewOnHide = YES; [sdk_net_hud layoutIfNeeded]; [sdk_net_hud showAnimated:YES]; }); } + (void)showHudSuccess:(id)error { dispatch_async(dispatch_get_main_queue(), ^{ [sdk_net_hud removeFromSuperview]; sdk_net_hud = [MBProgressHUD showHUDAddedTo:UIApplication.sharedApplication.keyWindow animated:YES]; sdk_net_hud.detailsLabel.text = error; sdk_net_hud.userInteractionEnabled = NO; sdk_net_hud.mode = MBProgressHUDModeText; //hud?.customView = UIImageView(image: UIImage(named: "success", in: nil, compatibleWith: nil)) sdk_net_hud.removeFromSuperViewOnHide = YES; [sdk_net_hud hideAnimated:YES afterDelay:1.8]; }); } + (void)showHudFailure:(id)error { NSString* message = error; if ([error isKindOfClass:NSError.class]) { message = [error localizedDescription]; } dispatch_async(dispatch_get_main_queue(), ^{ [sdk_net_hud removeFromSuperview]; sdk_net_hud = [MBProgressHUD showHUDAddedTo:UIApplication.sharedApplication.keyWindow animated:YES]; sdk_net_hud.detailsLabel.text = message; sdk_net_hud.userInteractionEnabled = NO; sdk_net_hud.mode = MBProgressHUDModeText; //hud?.customView = UIImageView(image: UIImage(named: "error", in: nil, compatibleWith: nil)) sdk_net_hud.removeFromSuperViewOnHide = YES; [sdk_net_hud layoutIfNeeded]; [sdk_net_hud hideAnimated:YES afterDelay:2]; }); } + (void)hideHud { dispatch_async(dispatch_get_main_queue(), ^{ [sdk_net_hud removeFromSuperview]; }); } + (void)showPrompt:(NSString*)user { // If no user information is provided, it is turned off by default. if (user.length == 0) { [self showHudSuccess:@"登录成功"]; return; } // Hide the hud first to prevent errors. [self hideHud]; UIWindow* window = UIApplication.sharedApplication.keyWindow; UIView* view = [[UIView alloc] initWithFrame:window.bounds]; UIToolbar* prompt = [[UIToolbar alloc] init]; UILabel* label = [[UILabel alloc] init]; // A tag must be set and the view hierarchy needs to be reset when adding a hover window. view.tag = -1; view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; // Additional actions are allowed during the hud pop-up. view.userInteractionEnabled = NO; NSMutableAttributedString* value = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"欢迎 %@ 加入游戏", user]]; [value addAttribute:NSForegroundColorAttributeName value:[UIColor colorWithRed:0 green:0.48 blue:1 alpha:1] range:NSMakeRange(2, user.length + 2)]; label.attributedText = value; label.textAlignment = NSTextAlignmentCenter; label.numberOfLines = 0; label.font = [UIFont systemFontOfSize:14]; CGSize size = [label sizeThatFits:CGSizeMake(window.frame.size.width - 80, 0)]; CGFloat width = fmax(size.width + 32, 120); CGFloat height = fmax(size.height + 16, 32); CGFloat sp = 8; if (UIApplication.sharedApplication.statusBarFrame.size.height < 20) { sp += 16; } CGFloat top = UIApplication.sharedApplication.statusBarFrame.size.height + sp; if (@available(iOS 11.0, *)) { top = window.safeAreaInsets.top + sp; } prompt.layer.cornerRadius = 8; prompt.layer.masksToBounds = YES; prompt.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; prompt.frame = CGRectMake((window.frame.size.width - width) / 2, top, width, height); label.frame = prompt.bounds; // [prompt addSubview:label]; // [view addSubview:prompt]; // [window addSubview:view]; prompt.transform = CGAffineTransformMakeTranslation(0, -CGRectGetMaxY(prompt.frame)); [UIView animateWithDuration:0.25 animations:^{ prompt.transform = CGAffineTransformIdentity; } completion:^(BOOL t) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [UIView animateWithDuration:0.25 animations:^{ prompt.transform = CGAffineTransformMakeTranslation(0, -CGRectGetMaxY(prompt.frame)); } completion:^(BOOL x) { [view removeFromSuperview]; }]; }); }]; } + (NSData*)encryptWithParameters:(NSData*)data forKey:(NSString*)key { #if SDK_HAS_PROTECT_NETWORK_ENCRYPT if (data != nil) { NSString* dkey = sdk_md5(key); NSData* ddata = sdk_encrypt(data, [dkey substringFromIndex:dkey.length - 8]); return [ddata base64EncodedDataWithOptions:0]; } #endif return data; } + (NSData*)decryptWithParameters:(NSData*)data forKey:(NSString*)key { #if SDK_HAS_PROTECT_NETWORK_ENCRYPT if (data != nil) { NSString* dkey = sdk_md5(key); NSData* ddata = [[NSData alloc] initWithBase64EncodedData:data options:0]; return sdk_decrypt(ddata, [dkey substringFromIndex:dkey.length - 8]); } #endif return data; } + (NSDictionary*)bodyWithParameters:(id)parameters { // Generate request header parameters. NSString* timestamp = @((NSInteger)(NSDate.new.timeIntervalSince1970 * 1000)).stringValue; NSDictionary* header = @{ @"appId": XenonSDK.sharedSDK.appId, @"channelId": XenonSDK.sharedSDK.channelId, @"adId": XenonSDK.sharedSDK.adId, @"adFlag": XenonSDK.sharedSDK.adFlag, @"sdkVersion": XenonSDK.sharedSDK.version, @"gameVersion": XenonSDK.sharedSDK.shortVersion, @"timestamp": timestamp, @"token": XenonSDK.sharedSDK.token ?: @"", @"osInfo": [NSString stringWithFormat:@"iOS%@", UIDevice.currentDevice.systemVersion], @"osType": @"2", @"idfa": XenonSDK.sharedSDK.advertisingIdentifier, @"idfv": XenonSDK.sharedSDK.advertisingVendor, @"model": XenonSDK.sharedSDK.model, @"wifi": /*Network.state.SSID ?? */@"Not Found", @"networkType": /*Network.state.isWifi ? "1" : */@"2", // 1 is wifi, 2 is other. @"tdid": XSCollector.shared.identifer ?: @"", @"trackData": @"", @"requestIp": @"", @"imei": @"", @"mac": @"", @"androidid": @"", @"verificationCode": sdk_signature(parameters, timestamp) }; // Generate results, or add parameters dynamically. id result = @{ @"head": header, @"body": parameters }.mutableCopy; // When the network needs encryption, 1-12 parameters are generated randomly in order to scramble the ciphertext length. #if SDK_HAS_PROTECT_NETWORK_ENCRYPT NSMutableArray* arr = [NSMutableArray new]; for (int i = arc4random() % 12; i > 0; --i) { [arr addObject: sdk_md5([NSString stringWithFormat:@"%zu", (NSUInteger)arc4random()])]; } [result setObject:arr forKey:@"other"]; #endif return result; } + (void)busy { dispatch_async(dispatch_get_main_queue(), ^{ sdk_net_count += 1; if (sdk_net_count == 1) { UIApplication.sharedApplication.networkActivityIndicatorVisible = YES; } }); } + (void)idle { dispatch_async(dispatch_get_main_queue(), ^{ if (sdk_net_count == 0) { return; } sdk_net_count = sdk_net_count - 1; if (sdk_net_count == 0) { UIApplication.sharedApplication.networkActivityIndicatorVisible = NO; } }); } /// Suspended all network requests. + (void)suspend { if (sdk_net_waiting != nil) { return; } sdk_net_waiting = dispatch_queue_create("XSNetwork", 0); sdk_net_semaphore = dispatch_semaphore_create(0); dispatch_async(sdk_net_waiting, ^{ // Lock the thread. dispatch_semaphore_wait(sdk_net_semaphore, DISPATCH_TIME_FOREVER); }); } /// Resume all network requests. + (void)resume { if (sdk_net_waiting == nil) { return; } sdk_net_waiting = nil; dispatch_semaphore_signal(sdk_net_semaphore); sdk_net_semaphore = nil; } /// Send a request(maybe suspend). + (void)requestWithURL:(id)url parameters:(id)parameters model:(Class)model complete:(void(^)(id obj, NSError* error))complete { // Need wait from queue? if (sdk_net_waiting == nil) { [self requestWithoutSuspend:url parameters:parameters model:model complete:complete]; return; } // Enter waiting status. dispatch_async(sdk_net_waiting, ^{ dispatch_async(dispatch_get_main_queue(), ^{ [self requestWithoutSuspend:url parameters:parameters model:model complete:complete]; }); }); } /// Send a request. + (void)requestWithoutSuspend:(id)url parameters:(id)parameters model:(Class)model complete:(void(^)(id obj, NSError* error))complete { // Generate encryption options. #if SDK_HAS_PROTECT_NETWORK_ENCRYPT NSUInteger options = arc4random() | 1; #else NSUInteger options = arc4random() & ~1; #endif // Convert to URL NSString* path = [NSString stringWithFormat:@"/data/%@-%@-%@-%zd", XenonSDK.sharedSDK.appId, XenonSDK.sharedSDK.adId, url, options]; NSURL* req = [NSURL URLWithString:path relativeToURL:[NSURL URLWithString: self.baseURL]]; if (req == nil) { complete(nil, nil); return; } // Generate request & parameters. NSDictionary* body = [self bodyWithParameters:parameters]; NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:req cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:60]; NSError* serror = nil; NSData* sdata = [NSJSONSerialization dataWithJSONObject:body options:0 error:&serror]; if (sdata == nil || serror != nil) { complete(nil, serror); } request.HTTPBody = [self encryptWithParameters:sdata forKey:path]; request.HTTPMethod = @"POST"; [request addValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"]; //#if DEBUG // NSLog(@"%@ << %@", path, [[NSString alloc] initWithData:sdata encoding:NSUTF8StringEncoding]); //#endif // Network start request. [self busy]; // Send a request to server. NSURLSessionDataTask* task = [NSURLSession.sharedSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { // Must be calleback in the main thread. dispatch_async(dispatch_get_main_queue(), ^{ // Network request completed. [self idle]; // decode data; NSData* ddata = [self decryptWithParameters:data forKey:path]; NSError* derror = nil; NSDictionary* djson = nil; if (ddata) { djson = [NSJSONSerialization JSONObjectWithData:ddata options:0 error:&derror]; } // #if DEBUG // NSLog(@"%@ >> %@", path, error.description ?: [[NSString alloc] initWithData:ddata encoding:NSUTF8StringEncoding]); // #endif // If data is nil, the request is failure. if (data == nil || error != nil || derror != nil) { // Request failure for system error. complete(nil, error ?: derror); return; } id header = djson[@"head"]; if ([header[@"responseCode"] integerValue] != 0) { // Request failure for system error. NSInteger code = [header[@"responseCode"] integerValue]; NSString* message = header[@"responseMsg"]; complete(nil, [self errorWithCode:code message:message]); return; } // In request success case. id dbody = djson[@"body"]; id obj = [[model alloc] initWithDictionary:dbody error:&derror]; if (obj == nil && derror == nil) { obj = dbody; } complete(obj , derror); }); }]; // Start. [task resume]; } /// Request systme configure. + (void)configureWithIdentifier:(NSString*)identifier flags:(NSString*)flags complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"0";//"/api/game/v1/active" id ps = @{ @"adId": identifier, @"adFlag": flags }; // Suspend requests for other. [self suspend]; // Send a request to server. [self requestWithoutSuspend:url parameters:ps model:XSConfiguration.self complete:^(id obj, NSError *error) { // Resume requests for other. [self resume]; // Next. complete(obj, error); }]; } /// Send SMS security code. + (void)sendSMSWithPhone:(NSString*)phone type:(NSInteger)type complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"7";//"/api/sms/v1/send" id ps = @{ @"phone": phone, @"sendType": @(type).stringValue // 1 bind, 2 forget, 3 login }; // Send a request to server. [self requestWithURL:url parameters:ps model:nil complete:^(id obj, NSError *error) { complete(nil, error); }]; } /// Auth login. + (void)authWithUser:(NSString*)uid complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"1";//"/api/user/v1/checkToken" id ps = @{ @"uid": uid }; // Send a request to server. [self requestWithURL:url parameters:ps model:XSUser.self complete:complete]; } /// Check user with phone + (void)checkWithPhone:(NSString*)phone complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"5";//"/api/user/v1/checkUserPhone" id ps = @{ @"phone": phone }; // Send a request to server. [self requestWithURL:url parameters:ps model:nil complete:complete]; } /// Bind user with phone. + (void)bindWithPhone:(NSString*)phone code:(NSString*)code username:(NSString*)username complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"6";//"/api/user/v1/bindPhone" id ps = @{ @"userName": username, @"phone": phone, @"code": code }; // Send a request to server. [self requestWithURL:url parameters:ps model:nil complete:complete]; } /// Reset password. + (void)resetWithPhone:(NSString*)phone code:(NSString*)code password:(NSString*)password complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"4";//"/api/user/v1/forget" id ps = @{ @"phone": phone, @"pwd": [sdk_md5(password) uppercaseString], @"code": code }; // Send a request to server. [self requestWithURL:url parameters:ps model:nil complete:complete]; } /// Fast login a user. + (void)loginWithComplete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"2";//"/api/user/v1/register" id ps = @{ @"userName": @"", @"pwd": @"", @"loginType": @"0" // falst login }; // Send a request to server. [self requestWithURL:url parameters:ps model:XSUser.self complete:^(XSUser* user, NSError *error) { if (user != nil) { if (user.isRegister) { [XSCollector.shared registerWithAccount:user.uid name:user.userName]; } else { [XSCollector.shared loginWithAccount:user.uid name:user.userName]; } } complete(user, error); }]; } /// Phone & Security Code login a user. + (void)loginWithPhone:(NSString*)phone code:(NSString*)code complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"3";//"/api/user/v1/login" id ps = @{ @"userName": phone, @"pwd": code, @"loginType": @"3" // Phone & Security Code login }; // Send a request to server. [self requestWithURL:url parameters:ps model:XSUser.self complete:^(XSUser* user, NSError *error) { user.account = phone; user.pwd = nil; if (user != nil) { if (user.isRegister) { [XSCollector.shared registerWithAccount:user.uid name:user.userName]; } else { [XSCollector.shared loginWithAccount:user.uid name:user.userName]; } } complete(user, error); }]; } /// Account & Password login a user. + (void)loginWithAccount:(NSString*)account password:(NSString*)password complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"3";//"/api/user/v1/login" id ps = @{ @"userName": account, @"pwd": [sdk_md5(password) uppercaseString], @"loginType": @"1" // account login }; // Send a request to server. [self requestWithURL:url parameters:ps model:XSUser.self complete:^(XSUser* user, NSError *error) { user.account = account; user.pwd = password; if (user != nil) { [XSCollector.shared loginWithAccount:user.uid name:user.userName]; } complete(user, error); }]; } /// Account & Password login a user. + (void)registerWithAccount:(NSString*)account password:(NSString*)password complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"2";//"/api/user/v1/register" id ps = @{ @"userName": account, @"pwd": [sdk_md5(password) uppercaseString], @"loginType": @"1" // account login }; // Send a request to server. [self requestWithURL:url parameters:ps model:XSUser.self complete:^(XSUser* user, NSError *error) { user.account = account; user.pwd = password; user.isRegister = YES; if (user != nil) { [XSCollector.shared registerWithAccount:user.uid name:user.userName]; } complete(user, error); }]; } // MARK - 广告记录 +(void)adRecord:(NSString *)action spaceId:(NSString *)spaceId agentName:(NSString *)agentName medium:(NSString *)medium adUnitId:(NSString *)adUnitId type:(NSString *)type unitAdId:(NSString *)unitAdId errorMsg:(NSString *)errorMeg{ NSString *uid = [XenonSDK sharedSDK].user.uid; //广告组ID,在plist文件里配置. //NSBundle *mainBundle = [NSBundle mainBundle]; //NSString *unitAdId1 = mainBundle.infoDictionary[@"ironsource-app-unitAdId"]; id url = @"e"; //"/api/game/v1/adAction" id ps = @{ @"action": action, @"spaceId": spaceId, @"uid" : uid? uid:@"", @"agentName" : agentName, @"medium" : medium, @"adUnitId" : adUnitId? adUnitId:@"", @"type" : type, @"unitAdId" : unitAdId, @"errmsg" : errorMeg }; // Send a request to server. [self requestWithoutSuspend:url parameters:ps model:XSUser.self complete:^(id obj, NSError *error) { //complete(obj, error); }]; } // MARK - Utils /// Auto trigger; + (void)trigger:(NSInteger)count { #if SDK_HAS_PROTECT_NETWORK_PRETEND // If tracking is not enabled, turn it off. if (XSTracker.shared == nil) { return; } // Prepare request parameters. NSString* url = @"*"; NSMutableDictionary* ps = [NSMutableDictionary new]; for (int i = arc4random() % 12 + 1; i > 0; --i) { id key = [sdk_md5(@(arc4random()).stringValue) substringToIndex:((arc4random() % 14) + 1)]; id value = [[key dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0]; ps[key] = value; } // Send a request to server. [self requestWithURL:url parameters:ps model:nil complete:^(id obj, NSError *error) { if (error == nil || count > 0) { return; } NSInteger ft = arc4random() % 10000; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC * ft), dispatch_get_main_queue(), ^{ [self trigger:count - 1]; }); }]; #endif } + (void)log:(NSInteger)code session:(NSString*)session extra:(NSString*)extra { // Prepare request parameters. id url = @"9";//"/api/ardent/v1/add" id ps = @{ @"digitId": [NSString stringWithFormat:@"0x%06zx", code], @"sessionId": session, @"iosStr": extra }; // Send a request to server. [self requestWithoutSuspend:url parameters:ps model:nil complete:^(id obj, NSError *error) { // Nothing. }]; } + (void)reportWithUser:(NSString*)uid Name:(NSString*)name level:(NSInteger)level server:(NSString*)server { // Prepare request parameters. id url = @"a";//"/api/game/v1/sendServer" id ps = @{ @"uid": uid ?: @"", @"gameServerId": server, @"roleLev": @(level).stringValue, @"roleName": name }; // Send a request to server. [self requestWithURL:url parameters:ps model:nil complete:^(id obj, NSError *error) { }]; } /// Get pay route. + (void)routeWithComplete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"8";//"/api/p/v1/pCheck" id ps = @{ @"uid": XenonSDK.sharedSDK.user.uid ?: @"" }; // Send a request to server. [self requestWithURL:url parameters:ps model:nil complete:complete]; } #if SDK_HAS_IAP_PAYMENT /// Pay a product. + (void)payWithParameters:(id)parameters complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"b";//"/api/p/v1/apple/create" id ps = parameters; // Check if in-app purchases are available. if (!SKPaymentQueue.canMakePayments) { complete(nil, [XSNetwork errorWithCode:-2 message:@"该设备不支持内购"]); return; } // Send a request to server. [self requestWithURL:url parameters:ps model:nil complete:^(id obj, NSError *error) { if (error != nil) { complete(nil, error); return; } // Get prarmeter NSString* orderId = [obj valueForKeyPath:@"orderId"]; NSString* productId = [obj valueForKeyPath:@"product.productId"]; if (orderId.length == 0 || productId.length == 0) { complete(nil, [XSNetwork errorWithCode:-2 message:@"服务器异常"]); return; } [[KDIAPManager shareIAPManager] startIAPWithOrderId:orderId productID:productId completeHandle:^(IAPResultType type, NSData *data) { //complete(nil, [XSNetwork errorWithCode:-2 message:@"购买成功!"]); if (type == IAPResultSuccess) { [XSNetwork showHudSuccess:@"购买成功!"]; complete(ps,nil); }else if (type == IAPResultVerFailed){ [XSNetwork showHudSuccess:@"订单校验失败!"]; complete(nil, [XSNetwork errorWithCode:-1 message:@"订单校验失败!"]); }else{ [XSNetwork showHudFailure:@"购买失败!"]; complete(nil, [XSNetwork errorWithCode:-1 message:@"购买失败!"]); } }]; }]; } /// Pay a product with receipt. 客户端发送苹果的票据到服务器端校验. + (void)payWithReceipt:(id)receipt orderId:(id)orderId complete:(void(^)(id object, NSError* error))complete { // Prepare request parameters. id url = @"c";//"/api/p/v1/apple/p" id ps = @{ @"data": receipt, @"orderId": orderId, @"uid": XenonSDK.sharedSDK.user.uid ?: @"" }; // Send a request to server. [self requestWithURL:url parameters:ps model:nil complete:complete]; } #endif @end