This commit is contained in:
watrabi
2025-09-18 17:55:52 -04:00
commit 977f1ff4b8
15030 changed files with 17324420 additions and 0 deletions
@@ -0,0 +1,31 @@
//
// RBXFunctions.h
// RobloxMobile
//
// Created by Ashish Jain on 9/4/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface RBXFunctions : NSObject
#define NSLOG_PRETTY_FUNCTION NSLog(@"METHOD NAME - %@", [NSString stringWithCString:__PRETTY_FUNCTION__ encoding:NSASCIIStringEncoding])
+ (BOOL) isEmpty:(id)object;
+ (BOOL) isEmptyString:(NSString *)string;
+ (void) dispatchOnMainThread:(void(^)(void))block;
+ (void) dispatchOnBackgroundThread:(void(^)(void))block;
+ (void) dispatchAfter:(NSUInteger)seconds onMainThread:(void(^)(void))block;
+ (void) dispatchAfter:(NSUInteger)seconds onBackgroundThread:(void(^)(void))block;
+ (NSString*) URLEncodeStringFromString:(NSString *)string;
+ (NSString*) stringWithPercentEscape:(NSString*)string;
#pragma mark - Memory Size functions
+ (NSInteger) sizeOfObject:(id)object;
+ (NSInteger) sizeOfDictionary:(id)object;
+ (NSInteger) sizeOfArray:(NSArray *)myArray;
@end
+145
View File
@@ -0,0 +1,145 @@
//
// RBXFunctions.m
// RobloxMobile
//
// Created by Ashish Jain on 9/4/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import "RBXFunctions.h"
#import <malloc/malloc.h>
@implementation RBXFunctions
+ (BOOL) isEmpty:(id)object {
if (nil == object) {
return YES;
}
if ([object isKindOfClass:[NSNull class]]) {
return YES;
}
if ([object isKindOfClass:[NSArray class]]) {
NSArray *array = (NSArray *)object;
if (array.count == 0) {
return YES;
}
}
if ([object isKindOfClass:[NSDictionary class]]) {
NSDictionary *dict = (NSDictionary *)object;
if (dict.allKeys.count == 0) {
return YES;
}
}
if ([object isKindOfClass:[NSData class]]) {
NSData *data = (NSData *)object;
if (data.length == 0) {
return YES;
}
}
return NO;
}
+ (BOOL) isEmptyString:(NSString *)string {
if (nil == string) {
return YES;
}
if (string.length == 0) {
return YES;
}
return NO;
}
+ (void) dispatchOnMainThread:(void(^)(void))block {
if (nil != block) {
dispatch_async(dispatch_get_main_queue(), block);
}
}
+ (void) dispatchOnBackgroundThread:(void(^)(void))block {
if (nil != block) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);
}
}
+ (void) dispatchAfter:(NSUInteger)seconds onMainThread:(void(^)(void))block {
if (nil != block) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC)), dispatch_get_main_queue(), block);
}
}
+ (void) dispatchAfter:(NSUInteger)seconds onBackgroundThread:(void(^)(void))block {
if (nil != block) {
dispatch_after((int64_t)(seconds * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);
}
}
+ (NSString*) URLEncodeStringFromString:(NSString *)string
{
return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"!@#$%&*()+'\";:=,/?[] "]];
}
+ (NSString*)stringWithPercentEscape:(NSString*)string
{
return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"=,!$&'()*+;@?\n\"<>#\t :/"]];
}
#pragma mark - Memory Size functions
+ (NSInteger) sizeOfObject:(id)object
{
if ([object isKindOfClass:[NSArray class]]) {
return [self sizeOfArray:object];
} else if ([object isKindOfClass:[NSDictionary class]]) {
return [self sizeOfDictionary:object];
}
id obj = nil;
int totalSize = 0;
totalSize += malloc_size((__bridge const void *)(obj));
return totalSize;
}
+ (NSInteger) sizeOfDictionary:(id)object
{
if ([object isKindOfClass:[NSDictionary class]]) {
int totalSize = 0;
NSDictionary *myDict = (NSDictionary *)object;
for(NSString *key in myDict.allKeys)
{
if ([myDict objectForKey:key]) {
id obj = nil;
obj = [myDict objectForKey:key];
totalSize += malloc_size((__bridge const void *)(obj));
}
}
return totalSize;
}
return [self sizeOfObject:object];
}
+ (NSInteger) sizeOfArray:(NSArray *)myArray
{
id obj = nil;
int totalSize = 0;
for(obj in myArray)
{
totalSize += malloc_size((__bridge const void *)(obj));
}
return totalSize;
}
@end
+19
View File
@@ -0,0 +1,19 @@
//
// RBXUIUtil.h
// RobloxMobile
//
// Created by Ashish Jain on 9/4/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface RBXUIUtil : NSObject
+ (void) increaseTappableAreaOfButton:(UIButton *)button;
+ (void) view:(UIView *)view setOrigin:(CGPoint)newOrigin;
+ (void) view:(UIView *)view setSize:(CGSize)newSize;
@end
+45
View File
@@ -0,0 +1,45 @@
//
// RBXUIUtil.m
// RobloxMobile
//
// Created by Ashish Jain on 9/4/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import "RBXUIUtil.h"
#import "RBXFunctions.h"
#import "UIButton+Extensions.h"
@implementation RBXUIUtil
+ (void) increaseTappableAreaOfButton:(UIButton *)button {
if ([RBXFunctions isEmpty:button]) {
return;
}
// Used in conjunction with UIButton+Extensions.h to increase the tappable area of the button without increasing the size of the button
[button setHitTestEdgeInsets:UIEdgeInsetsMake(-10, -10, -10, -10)];
}
+ (void) view:(UIView *)view setOrigin:(CGPoint)newOrigin {
if ([RBXFunctions isEmpty:view]) {
return;
}
CGRect frame = view.frame;
frame.origin = newOrigin;
view.frame = frame;
}
+ (void) view:(UIView *)view setSize:(CGSize)newSize {
if ([RBXFunctions isEmpty:view]) {
return;
}
CGRect frame = view.frame;
frame.size = newSize;
view.frame = frame;
}
@end
@@ -0,0 +1,51 @@
//
// StoreManager.h
// newapp
//
// Created by Dongwei Liao on 9/5/12.
// Copyright (c) 2012 roblox. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
#import "StandaloneAppStore.h"
// this is used for getting the current store, as we only want
// to use one store for each app, either StoreManager
// which is the RobloxMobile store (does receipt verifying)
// or StandaloneAppStore which is used by standalone apps (no receipt verifying, apps do that)
#ifdef STANDALONE_PURCHASING
#define GetStoreMgr [StandaloneAppStore getStandaloneAppStore]
#else
#define GetStoreMgr [StoreManager getStoreMgr]
#endif
@class UpgradesViewController;
@interface StoreManager : NSObject <SKProductsRequestDelegate, SKPaymentTransactionObserver>
{
int retries;
int timeIntervalBetweenRobuxPurchaseInMinutes;
int timeIntervalBetweenBCPurchaseInMinutes;
int timeIntervalBetweenCatalogPurchaseInMinutes;
int timeLimitForRetryOfBillingServiceBeforeGivingUp;
bool allowAppleInAppPurchase;
bool readInAppPurchaseSettingsBeforeEveryPurchase;
shared_ptr<RBX::Instance> playerPurchasing;
std::string productIdPurchasing;
enum ReceiptStatus { ok, error, bogus };
}
-(void) signalMarketplacePurchaseFinished:(bool) purchased;
-(void) promptNativePurchase:(shared_ptr<RBX::Instance>) player productId:(std::string) productId;
-(BOOL) checkForInAppPurchases:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
-(void) purchaseProduct:(NSString*)productId;
-(BOOL) restrictTimeBoundPurchase:(NSString*)productId;
-(void) recordPurchaseTime:(NSString*)productId;
// WARNING: DO NOT access this directly!!!!! please use the macro 'GetStoreMgr' instead
+(StoreManager*) getStoreMgr;
@end
+800
View File
@@ -0,0 +1,800 @@
//
// StoreManager.m
// newapp
//
// Created by Dongwei Liao on 9/5/12.
// Copyright (c) 2012 roblox. All rights reserved.
//
#import <Foundation/NSJSONSerialization.h>
#import "AppDelegate.h"
#import "StoreManager.h"
#import "RobloxAlert.h"
#import "UserInfo.h"
#import "RobloxInfo.h"
#import "RobloxGoogleAnalytics.h"
#import "iOSSettingsService.h"
#import "Reachability.h"
#import "RobloxWebUtility.h"
#import "RobloxNotifications.h"
#import "PlaceLauncher.h"
#include "v8datamodel/DataModel.h"
#include "v8datamodel/MarketplaceService.h"
#include "Network/Players.h"
#define NUM_RETRIES_BEFORE_INFORM_USER 10
#define TOTAL_NUM_RETRIES 200
@implementation StoreManager
- (id) init
{
if(self = [super init])
{
// set these to some default value (should get them from app settings pretty quick though)
timeIntervalBetweenBCPurchaseInMinutes = 5;
timeIntervalBetweenCatalogPurchaseInMinutes = 5;
timeLimitForRetryOfBillingServiceBeforeGivingUp = 20;
allowAppleInAppPurchase = true;
readInAppPurchaseSettingsBeforeEveryPurchase = false;
#ifdef NATIVE_IOS
timeIntervalBetweenRobuxPurchaseInMinutes = 0;
#else
timeIntervalBetweenRobuxPurchaseInMinutes = 5;
#endif
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
iOSSettingsService * iosSettings = [[RobloxWebUtility sharedInstance] getCachediOSSettings];
#ifdef NATIVE_IOS
timeIntervalBetweenRobuxPurchaseInMinutes = iosSettings->GetValueRobloxHDTimeIntervalBetweenRobuxPurchaseInMinutes();
#else
timeIntervalBetweenRobuxPurchaseInMinutes = iosSettings->GetValueTimeIntervalBetweenRobuxPurchaseInMinutes();
#endif
timeIntervalBetweenBCPurchaseInMinutes = iosSettings->GetValueTimeIntervalBetweenBCPurchaseInMinutes();
timeIntervalBetweenCatalogPurchaseInMinutes = iosSettings->GetValueTimeIntervalBetweenCatalogPurchaseInMinutes();
timeLimitForRetryOfBillingServiceBeforeGivingUp = iosSettings->GetValueTimeLimitForBillingServiceRetriesBeforeGivingUp();
allowAppleInAppPurchase = iosSettings->GetValueAllowAppleInAppPurchase();
readInAppPurchaseSettingsBeforeEveryPurchase = iosSettings->GetValueReadInAppPurchaseSettingsBeforeEveryPurchase();
});
retries = 0;
[self resetPlayerPurchasing];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(gotDidLeaveGameNotification:) name:RBX_NOTIFY_GAME_DID_LEAVE object:nil ];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
// Initialize this after Login, do not initialize on Application Start up, the purchase should go to Roblox Authenticated User
+ (StoreManager*)getStoreMgr
{
static dispatch_once_t storePred = 0;
__strong static StoreManager* _sharedStoreManager = nil;
dispatch_once(&storePred, ^{ // Need to use GCD for thread-safe allocation of singleton
_sharedStoreManager = [[self alloc] init];
});
return _sharedStoreManager;
}
-(void) signalMarketplacePurchaseFinished:(bool) purchased
{
if (playerPurchasing.get() && !productIdPurchasing.empty())
{
if (RBX::MarketplaceService* marketService = RBX::ServiceProvider::find<RBX::MarketplaceService>(playerPurchasing.get()))
{
marketService->signalPromptNativePurchaseFinished(playerPurchasing, productIdPurchasing, purchased);
[self resetPlayerPurchasing];
}
}
}
- (BOOL) canMakePurchase
{
[[UserInfo CurrentPlayer] UpdatePlayerInfo];
return [SKPaymentQueue canMakePayments];
}
-(void) resetPlayerPurchasing
{
playerPurchasing = shared_ptr<RBX::Instance>();
productIdPurchasing = "";
}
- (void) request:(SKRequest*)request didFailWithError:(NSError *)error
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"StoreManager: Could not contact App Store properly because %s", [[error localizedDescription] cStringUsingEncoding:NSUTF8StringEncoding]);
}
- (void) requestDidFinish:(SKRequest*)request
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"StoreManager: Request finished");
}
// Restrict Time Bound Purchase only for Robux & BC Purchases, all catalog purchases should go through without restrictions.
-(BOOL) restrictTimeBoundPurchase:(NSString *)productId
{
if (readInAppPurchaseSettingsBeforeEveryPurchase)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
iOSSettingsService * iosSettings = [[RobloxWebUtility sharedInstance] getCachediOSSettings];
#ifdef NATIVE_IOS
timeIntervalBetweenRobuxPurchaseInMinutes = iosSettings->GetValueRobloxHDTimeIntervalBetweenRobuxPurchaseInMinutes();
#else
timeIntervalBetweenRobuxPurchaseInMinutes = iosSettings->GetValueTimeIntervalBetweenRobuxPurchaseInMinutes();
#endif
timeIntervalBetweenBCPurchaseInMinutes = iosSettings->GetValueTimeIntervalBetweenBCPurchaseInMinutes();
timeIntervalBetweenCatalogPurchaseInMinutes = iosSettings->GetValueTimeIntervalBetweenCatalogPurchaseInMinutes();
timeLimitForRetryOfBillingServiceBeforeGivingUp = iosSettings->GetValueTimeLimitForBillingServiceRetriesBeforeGivingUp();
allowAppleInAppPurchase = iosSettings->GetValueAllowAppleInAppPurchase();
readInAppPurchaseSettingsBeforeEveryPurchase = iosSettings->GetValueReadInAppPurchaseSettingsBeforeEveryPurchase();
});
}
if(!allowAppleInAppPurchase)
return NO;
BOOL retVal = YES;
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
if([productId hasSuffix:@"Robux"])
{
NSTimeInterval lastPurchaseTimeRobux = [[NSUserDefaults standardUserDefaults] doubleForKey:@"LastPurchaseTimeRobux"];
// Block Robux Purchase for 10 minutes between purchases
if(lastPurchaseTimeRobux != 0 && currentTime < (lastPurchaseTimeRobux + 60 * timeIntervalBetweenRobuxPurchaseInMinutes))
retVal = NO;
}
else if([productId hasSuffix:@"monthBC"] || [productId hasSuffix:@"monthOBC"] || [productId hasSuffix:@"monthTBC"])
{
NSTimeInterval lastPurchaseTimeBC = [[NSUserDefaults standardUserDefaults] doubleForKey:@"LastPurchaseTimeBC"];
// Block BC Purchases for 24 hours between purchases
if(lastPurchaseTimeBC != 0 && currentTime < (lastPurchaseTimeBC + 60 * timeIntervalBetweenBCPurchaseInMinutes))
retVal = NO;
}
else // This is a Catalog purchase
{
NSTimeInterval lastPurchaseTimeCatalog = [[NSUserDefaults standardUserDefaults] doubleForKey:productId];
// Block Individual Catalog Purchases for 10 Minutes between purchases
if(lastPurchaseTimeCatalog != 0 && currentTime < (lastPurchaseTimeCatalog + 60 * timeIntervalBetweenCatalogPurchaseInMinutes))
retVal = NO;
}
return retVal;
}
-(void) reset
{
retries = 0;
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"PendingTransactionUserName"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"BillingServiceRetries"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"RecordedTimeForBillingServiceFirstFailure"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
// Record the Purchase Time,s o that we can restrict Time Bound Purchase with ios Client Settings adjustments.
-(void) recordPurchaseTime:(NSString *)productId
{
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
if([productId hasSuffix:@"Robux"])
[[NSUserDefaults standardUserDefaults] setDouble:currentTime forKey:@"LastPurchaseTimeRobux"];
else if([productId hasSuffix:@"monthBC"] || [productId hasSuffix:@"monthOBC"] || [productId hasSuffix:@"monthTBC"])
[[NSUserDefaults standardUserDefaults] setDouble:currentTime forKey:@"LastPurchaseTimeBC"];
else // This is a Catalog Purchase
[[NSUserDefaults standardUserDefaults] setDouble:currentTime forKey:productId];
[[NSUserDefaults standardUserDefaults] synchronize];
}
-(void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
if ([self canMakePurchase])
{
NSInteger numProduct = [response.products count];
SKProduct* product_ = [response.products lastObject];
if (!product_)
{
[self signalMarketplacePurchaseFinished:false];
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"Attempt" withLabel:@"NoProduct" withValue:0];
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"FailureAppStore" withLabel:@"NoProduct" withValue:0];
NSString *message = [NSString stringWithFormat:@"Num products: %ld, no products found", (long)numProduct];
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_WARNING,"StoreManager: %s",[message cStringUsingEncoding:NSUTF8StringEncoding]);
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"CannotMakePurchase", nil)];
}
else
{
NSInteger numTransaction = [[[SKPaymentQueue defaultQueue] transactions] count];
if(numTransaction > 0)
{
SKPaymentTransaction *transaction = [[[SKPaymentQueue defaultQueue] transactions] objectAtIndex:0];
[self completeTransaction:transaction];
return;
}
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"Attempt" withLabel:product_.productIdentifier withValue:0];
NSString* pendingTransUserName = [UserInfo CurrentPlayer].username;
if([pendingTransUserName length] != 0)
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"StoreManager: Product ID: %s", [product_.productIdentifier cStringUsingEncoding:NSUTF8StringEncoding]);
BOOL proceed = [self restrictTimeBoundPurchase:product_.productIdentifier];
if(proceed == YES)
{
SKPayment* payment = [SKPayment paymentWithProduct:product_];
[[SKPaymentQueue defaultQueue] addPayment:payment];
pendingTransUserName = [UserInfo CurrentPlayer].username;
[[NSUserDefaults standardUserDefaults] setObject:pendingTransUserName forKey:@"PendingTransactionUserName"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
else
{
[self signalMarketplacePurchaseFinished:false];
if([product_.productIdentifier hasSuffix:@"Robux"])
[RobloxAlert RobloxAlertWithMessage:[NSString stringWithFormat:NSLocalizedString(@"WaitForAnotherRobuxPurchase", nil)]];
else if([product_.productIdentifier hasSuffix:@"monthBC"] || [product_.productIdentifier hasSuffix:@"monthOBC"] || [product_.productIdentifier hasSuffix:@"monthTBC"])
[RobloxAlert RobloxAlertWithMessage:[NSString stringWithFormat:NSLocalizedString(@"WaitForAnotherBCPurchase", nil)]];
else // Catalog Purchase
[RobloxAlert RobloxAlertWithMessage:[NSString stringWithFormat:NSLocalizedString(@"WaitForAnotherCatalogPurchase", nil)]];
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"FailureAppStore" withLabel:@"WaitForAnotherPurchaseTimeDelay" withValue:0];
}
}
else
{
[self signalMarketplacePurchaseFinished:false];
NSString *message = [NSString stringWithFormat:NSLocalizedString(@"SessionExpired", nil)];
[RobloxAlert RobloxAlertWithMessage:message];
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"FailureAppStore" withLabel:product_.productIdentifier withValue:0];
}
}
}
else
{
[self signalMarketplacePurchaseFinished:false];
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"CannotPurchaseBecauseParentalControl", nil)];
}
}
-(void) requestProductData:(NSString* )productId
{
NSSet* set = [NSSet setWithObject:productId];
SKProductsRequest* request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
request.delegate = self;
[request start];
}
-(void) purchaseProduct:(NSString*)productId
{
if (![self canMakePurchase])
{
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"CannotPurchaseBecauseParentalControl", nil)];
}
else
{
[self requestProductData:productId];
}
}
enum
{
Transaction_PendingMatchesCurrentUser = 0,
Transaction_PendingNotMatchesCurrentUser,
Transaction_PendingUsernameLost,
};
-(int) verifyIfCorrectUser
{
if (![[NSUserDefaults standardUserDefaults] objectForKey:@"PendingTransactionUserName"])
{
return Transaction_PendingUsernameLost;
}
else if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"PendingTransactionUserName"] isEqualToString:@""])
{
return Transaction_PendingUsernameLost;
}
NSString *userNameOfPendingTransaction = [[NSUserDefaults standardUserDefaults] stringForKey:@"PendingTransactionUserName"];
UserInfo* user = [UserInfo CurrentPlayer];
NSString *userNameOfCurrentLoggedInUser = user.username;
if(![user userLoggedIn])// It means the user is logged out.
userNameOfCurrentLoggedInUser = @"";
if ([userNameOfCurrentLoggedInUser isEqualToString:userNameOfPendingTransaction])
return Transaction_PendingMatchesCurrentUser;
return Transaction_PendingNotMatchesCurrentUser;
}
// This is called whenevr we have unfinished transaction in the Apple Store Queue
// Also called when we do retries in case of Roblox Billing Service failure.
-(void) completeTransaction:(SKPaymentTransaction*)transaction
{
if (transaction == nil)
{
return;
}
Reachability* reachability = [Reachability reachabilityForInternetConnection];
NetworkStatus remoteStatus = [reachability currentReachabilityStatus];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSData* receipt = transaction.transactionReceipt;
#pragma clang diagnostic pop
if (receipt == nil) // Apple Bug: iOS sometimes will give you the nil receipt even though Queue is empty, exit gracefully
return;
if (remoteStatus == NotReachable)
{
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"ConnectionError", nil)];
[self performSelector:@selector(completeTransaction:) withObject:transaction afterDelay:60*10];
return;
}
int transactionUserState = [self verifyIfCorrectUser];
if (transactionUserState == Transaction_PendingMatchesCurrentUser)
{
[self verifyReceipt:receipt forProductId:transaction.payment.productIdentifier paymentTransaction:transaction paymentQueue:[SKPaymentQueue defaultQueue]];
}
else if (transactionUserState == Transaction_PendingNotMatchesCurrentUser)
{
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
NSTimeInterval timeofFirstFailure = [[NSUserDefaults standardUserDefaults] doubleForKey:@"RecordedTimeForBillingServiceFirstFailure"];
// If Time elapsed is more than than the retries limit set with billing service, grant the current user the item.
if(timeofFirstFailure != 0 && currentTime > (timeofFirstFailure + 60 * 60 * timeLimitForRetryOfBillingServiceBeforeGivingUp))
{
[self verifyReceipt:receipt forProductId:transaction.payment.productIdentifier paymentTransaction:transaction paymentQueue:[SKPaymentQueue defaultQueue]];
}
else
{
[self performSelector:@selector(completeTransaction:) withObject:transaction afterDelay:60*10];
}
}
else if (transactionUserState == Transaction_PendingUsernameLost)
{
if ([[UserInfo CurrentPlayer] userLoggedIn])
{
[self verifyReceipt:receipt forProductId:transaction.payment.productIdentifier paymentTransaction:transaction paymentQueue:[SKPaymentQueue defaultQueue]];
}
else
{
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"AnyUserNeedsToSignIn", nil)];
[self performSelector:@selector(completeTransaction:) withObject:transaction afterDelay:60*10];
}
}
}
// Transaction probably verified with Roblox Billing Service
// Finish the Transaction from Apple Store Kit if successful verification
// Otherwise keep retrying with Roblox billing service calls
-(void) endTransaction: (ReceiptStatus) verified paymentTransaction:(SKPaymentTransaction*) transaction paymentQueue:(SKPaymentQueue*) paymentQueue
{
if (verified == ReceiptStatus::ok)
{
[paymentQueue finishTransaction: transaction];
[self reset];
[self signalMarketplacePurchaseFinished:true];
// Record the purchase time of the successful transaction
[self recordPurchaseTime:transaction.payment.productIdentifier];
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"Success" withLabel:transaction.payment.productIdentifier withValue:0];
if (retries)
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"SuccessBillingServiceAfterRetry" withLabel:transaction.payment.productIdentifier withValue:retries];
else
{
NSInteger retriesRecorded = [[NSUserDefaults standardUserDefaults] integerForKey:@"BillingServiceRetries"];
if (retriesRecorded)
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"SuccessBillingServiceAfterRelaunch" withLabel:transaction.payment.productIdentifier withValue:1];
}
}
else if(verified == ReceiptStatus::bogus)
{
[paymentQueue finishTransaction: transaction];
[self reset];
[self signalMarketplacePurchaseFinished:false];
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"Bogus" withLabel:transaction.payment.productIdentifier withValue:0];
}
else
{
retries++;
if(retries == 1)
{
NSInteger retriesRecorded = [[NSUserDefaults standardUserDefaults] integerForKey:@"BillingServiceRetries"];
if (retriesRecorded)
{
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"FailureBillingServiceAfterRelaunch" withLabel:transaction.payment.productIdentifier withValue:1];
}
else
{
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"FailureBillingService1stRetry" withLabel:transaction.payment.productIdentifier withValue:1];
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
[[NSUserDefaults standardUserDefaults] setDouble:currentTime forKey:@"RecordedTimeForBillingServiceFirstFailure"];
}
}
else
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"FailureBillingServiceMultipleRetry" withLabel:transaction.payment.productIdentifier withValue:retries];
[[NSUserDefaults standardUserDefaults] setInteger:retries forKey:@"BillingServiceRetries"];
// Retry reached 10 attempts, inform user that his transaction is not working, and keep trying the transaction.
if (retries == NUM_RETRIES_BEFORE_INFORM_USER)
{
dispatch_sync(dispatch_get_main_queue(), ^{
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"TransactionDelayedBody", nil)];
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"FailureBillingServiceTooManyRetries" withLabel:transaction.payment.productIdentifier withValue:1];
});
}
// We will keep retrying for 200 times with increamenting the interval by 10 seconds on each failure
// Will give up retrying in 48 hours as recorded with 1st retry
if(retries <= TOTAL_NUM_RETRIES)
{
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
NSTimeInterval timeofFirstFailure = [[NSUserDefaults standardUserDefaults] doubleForKey:@"RecordedTimeForBillingServiceFirstFailure"];
// Try to make the billing service attempt for 48 hours, can be changed with client settings
if(timeofFirstFailure != 0 && currentTime < (timeofFirstFailure + 60 * 60 * timeLimitForRetryOfBillingServiceBeforeGivingUp))
{
#ifdef _DEBUG
// Do not keep retrying in DEBUG build, just clear out the transaction if it failed on the 1st attempt.
[paymentQueue finishTransaction: transaction];
[self reset];
return;
#endif
dispatch_sync(dispatch_get_main_queue(), ^{
[self performSelector:@selector(completeTransaction:) withObject:transaction afterDelay:10*retries];
});
}
else// If this is a bogus receipt, we want to remove the transaction after 48 hours
{
dispatch_sync(dispatch_get_main_queue(), ^{
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"TransactionFailedBody", nil)];
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"FailureBillingServiceTooManyRetries" withLabel:transaction.payment.productIdentifier withValue:1];
});
[self signalMarketplacePurchaseFinished:false];
[paymentQueue finishTransaction: transaction];
[self reset];
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"FailureDueToBogusReceipt" withLabel:transaction.payment.productIdentifier withValue:0];
}
}
}
}
// Transaction not yet initiated, it failed before being charged, it can be user clicked Cancel, or incorect password, etc
-(void) failedTransaction:(SKPaymentTransaction*)transaction
{
#ifndef NDEBUG
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"StoreManager: User cancelled transaction: %s", [[transaction description] cStringUsingEncoding:NSUTF8StringEncoding]);
#endif
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
[self reset];
[self signalMarketplacePurchaseFinished:false];
if (transaction.transactionState == SKPaymentTransactionStateFailed)
{
NSError *err = transaction.error;
NSString *desc = [err description];
NSLog(desc, nil);
if ([err code] == 2)
[RobloxGoogleAnalytics setEventTracking:@"Purchase" withAction:@"FailureAppStoreDueToCancelOrUserAuthentication" withLabel:transaction.payment.productIdentifier withValue:0];
}
}
-(void) restoreTransaction:(SKPaymentTransaction*)transaction
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"StoreManager: restore transaction ...");
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
// SKObserver
-(void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
default:
break;
}
}
}
- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length
{
static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
NSMutableData *data = [NSMutableData dataWithLength:((length + 2) / 3) * 4];
uint8_t *output = (uint8_t *)data.mutableBytes;
for (NSInteger i = 0; i < length; i += 3) {
NSInteger value = 0;
for (NSInteger j = i; j < (i + 3); j++) {
value <<= 8;
if (j < length) {
value |= (0xFF & input[j]);
}
}
NSInteger index = (i / 3) * 4;
output[index + 0] = table[(value >> 18) & 0x3F];
output[index + 1] = table[(value >> 12) & 0x3F];
output[index + 2] = (i + 1) < length ? table[(value >> 6) & 0x3F] : '=';
output[index + 3] = (i + 2) < length ? table[(value >> 0) & 0x3F] : '=';
}
NSString* s = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
NSLog(@"This is the current iTunes purchase receipt:\n%@\n", s);
return s;
}
// Verify the Apple Transaction receit with the Roblox Billing Service
-(void) verifyReceipt:(NSData*)receiptData forProductId:(NSString*)productId paymentTransaction:(SKPaymentTransaction*) transaction paymentQueue:(SKPaymentQueue*) paymentQueue
{
NSString* urlString = [RobloxInfo getWWWBaseUrl];
NSObject *testObject = [[NSUserDefaults standardUserDefaults] objectForKey:@"RecordedTimeForBillingServiceFirstFailure"];
if (testObject)
urlString = [urlString stringByAppendingString:@"mobileapi/apple-purchase?isRetry=true"];
else
urlString = [urlString stringByAppendingString:@"mobileapi/apple-purchase"];
urlString = [urlString stringByReplacingOccurrencesOfString:@"http:" withString:@"https:"];
NSURL *url = [NSURL URLWithString: urlString];
NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:60];
[RobloxInfo setDefaultHTTPHeadersForRequest:theRequest];
[theRequest setHTTPMethod:@"POST"];
[theRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
NSString *receiptDataString = [self encode:(uint8_t *)receiptData.bytes length:receiptData.length];
receiptDataString = [receiptDataString stringByReplacingOccurrencesOfString:@"+" withString: @"%2B"];
NSString *postData = [NSString stringWithFormat:@"receipt=%@&productId=%@", receiptDataString, productId];
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"StoreManager: Receipt Data String: %s", [receiptDataString cStringUsingEncoding:NSUTF8StringEncoding]);
NSString *length = [NSString stringWithFormat:@"%lu", (unsigned long)[postData length]];
[theRequest setValue:length forHTTPHeaderField:@"Content-Length"];
[theRequest setHTTPBody:[postData dataUsingEncoding:NSASCIIStringEncoding]];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:theRequest queue:queue completionHandler:
^(NSURLResponse *response, NSData *receiptResponseData, NSError *error)
{
ReceiptStatus retVal = ReceiptStatus::error;
NSHTTPURLResponse* urlResponse = ( NSHTTPURLResponse*) response;
if(urlResponse == nil)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"StoreManager: Login failed due to improper cast");
long responseStatusCode = [urlResponse statusCode];
NSString *responseString = [[NSString alloc] initWithData:receiptResponseData encoding:NSASCIIStringEncoding];
if(responseStatusCode == 200 && [responseString isEqualToString: @"OK"])
{
retVal = ReceiptStatus::ok;
// bring up home view or error alert in main synchronous thread (ui stuff must be loaded in main thread)
dispatch_sync(dispatch_get_main_queue(), ^{
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"PurchaseSuccessful", nil)];
#ifdef NATIVE_IOS
// this is a bit of a hack for now.
//we don't know _exactly_ how long it will take for the server to respond, so we err on the side of caution with 10sec.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:RBX_NOTIFY_ROBUX_UPDATED object:nil];
});
#endif
});
}
else if(responseStatusCode == 200 && [responseString isEqualToString: @"Bogus"])
retVal = ReceiptStatus::bogus;
[self endTransaction:retVal paymentTransaction:transaction paymentQueue:paymentQueue];
}];
}
-(void) purchasePrecheck:(NSString*)productId completion:(void(^)(BOOL success, NSString* error))handler
{
NSString* urlString = [NSString stringWithFormat:@"%@mobileapi/validate-mobile-purchase", [RobloxInfo getWWWBaseUrl]];
NSURL *url = [NSURL URLWithString: urlString];
NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:60];
[RobloxInfo setDefaultHTTPHeadersForRequest:theRequest];
[theRequest setHTTPMethod:@"POST"];
[theRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
NSString *postData = [NSString stringWithFormat:@"productId=%@", productId];
NSString *length = [NSString stringWithFormat:@"%lu", (unsigned long)[postData length]];
[theRequest setValue:length forHTTPHeaderField:@"Content-Length"];
[theRequest setHTTPBody:[postData dataUsingEncoding:NSASCIIStringEncoding]];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:theRequest queue:queue completionHandler:
^(NSURLResponse *response, NSData *responseData, NSError *error)
{
NSHTTPURLResponse* urlResponse = ( NSHTTPURLResponse*) response;
if(urlResponse == nil)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"StoreManager: Purchase precheck failed due to missing response.");
long responseStatusCode = [urlResponse statusCode];
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSASCIIStringEncoding];
if(responseStatusCode == 200)
{
if ([responseString isEqualToString: @"OK"])
handler(YES, nil);
else if ([responseString isEqualToString: @"Error"])
handler(NO, NSLocalizedString(@"PurchasingInvalidPurchase", nil));
else if ([responseString isEqualToString:@"Retry"])
handler(NO, NSLocalizedString(@"PurchasingBillingDown", nil));
else if ([responseString isEqualToString:@"Limit"])
handler(NO, NSLocalizedString(@"PurchasingLimit", nil));
else
handler(NO, NSLocalizedString(@"PurchasingUnknownError", nil));
}
else
handler(NO, NSLocalizedString(@"PurchasingUnknownError", nil));
}];
}
-(BOOL) checkForInAppPurchases:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
// handle in-app purchases
if (navigationType == UIWebViewNavigationTypeFormSubmitted)
{
[request mainDocumentURL];
}
else
{
NSURL *url = request.URL;
NSString *urlLoadStr = url.absoluteString;
NSString* urlBuy = [[RobloxInfo getWWWBaseUrl] stringByAppendingString:@"mobile-app-upgrades/buy"];
if ([urlLoadStr hasPrefix:urlBuy])
{
NSArray* tokens = [urlLoadStr componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]];
if (tokens.count == 2)
{
NSString* id = (NSString*)[tokens objectAtIndex:1];
[self purchasePrecheck:id completion:^(BOOL success, NSString *errorMessage) {
if (success)
dispatch_async(dispatch_get_main_queue(), ^{
[[StoreManager getStoreMgr] purchaseProduct:id];
});
else
[RobloxAlert RobloxAlertWithMessage:errorMessage];
}];
}
return YES;
}
}
return NO;
}
-(void) promptNativePurchase:(shared_ptr<RBX::Instance>) player productId:(std::string) productId
{
if (!player)
{
[self signalMarketplacePurchaseFinished:false];
return;
}
if (productId.empty())
{
[self signalMarketplacePurchaseFinished:false];
return;
}
if (productIdPurchasing.compare("") != 0)
{ [self signalMarketplacePurchaseFinished:false];
return;
}
if (playerPurchasing != shared_ptr<RBX::Instance>())
{
[self signalMarketplacePurchaseFinished:false];
return;
}
if (RBX::Network::Player* thePlayer = RBX::Instance::fastDynamicCast<RBX::Network::Player>(player.get()))
{
RBX::Network::Player* localPlayer = RBX::Network::Players::findLocalPlayer(RBX::DataModel::get(player.get()));
if (localPlayer == thePlayer)
{
productIdPurchasing = productId;
playerPurchasing = player;
[[StoreManager getStoreMgr] purchaseProduct:[NSString stringWithUTF8String:productId.c_str()]];
}
else
{
[self signalMarketplacePurchaseFinished:false];
}
}
else
{
[self signalMarketplacePurchaseFinished:false];
}
}
- (void) gotDidLeaveGameNotification:(NSNotification *)aNotification
{
[self resetPlayerPurchasing];
}
@end
@@ -0,0 +1,27 @@
//
// UpgradeCheckHelper.h
// RobloxMobile
//
// Created by Ben Tkacheff on 4/9/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
// This class is used by AppDelegate to determine whether our client version is up to date
// with what we offer in the app store. If not, this class will alert this user.
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface UpgradeCheckHelper : NSObject <UIAlertViewDelegate, NSURLConnectionDataDelegate>
{
UIAlertView* upgradeAlertView;
NSURLConnection* upgradeConnection;
NSMutableData* upgradeResponseData;
}
+ (UpgradeCheckHelper*) getUpgradeCheckHelper;
+ (NSString *) getUpgradeUrl;
+ (void) checkForUpdate:(NSString*) appName;
@end
@@ -0,0 +1,257 @@
//
// UpgradeCheckHelper.m
// RobloxMobile
//
// Created by Ben Tkacheff on 4/9/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import "UpgradeCheckHelper.h"
#import "Reachability.h"
#import "RobloxAlert.h"
#import "PlaceLauncher.h"
#import "RobloxInfo.h"
#include "Util/Http.h"
#include "util/standardout.h"
// semi-private methods
@interface UpgradeCheckHelper ()
- (void) makeUpgradeRequest:(NSURLRequest*) request;
- (UIButton*) getAlertViewButton:(NSString*) buttonTitle;
- (void) processCheckForUpdateResponse;
@end
@implementation UpgradeCheckHelper
+ (UpgradeCheckHelper*) getUpgradeCheckHelper
{
static dispatch_once_t upgradePred = 0;
__strong static UpgradeCheckHelper* _sharedUpgradeCheckHelper = nil;
dispatch_once(&upgradePred, ^{ // Need to use GCD for thread-safe allocation of singleton
_sharedUpgradeCheckHelper = [[self alloc] init];
});
return _sharedUpgradeCheckHelper;
}
- (id) init
{
if(self = [super init])
{
upgradeAlertView = [[UIAlertView alloc] init];
upgradeAlertView.delegate = self;
upgradeResponseData = [[NSMutableData alloc] init];;
upgradeConnection = nil;
}
return self;
}
+ (NSString *) getUpgradeUrl
{
return [[RobloxInfo getWWWBaseUrl] stringByAppendingString:@"mobileapi/check-app-version?appVersion=%@"];
}
- (UIButton*) getAlertViewButton:(NSString*) buttonTitle
{
for(UIView *view in upgradeAlertView.subviews)
{
if([view isKindOfClass:[UIButton class]])
{
UIButton* button = (UIButton*)view;
if([button.currentTitle isEqualToString:buttonTitle])
return button;
}
}
return nil;
}
- (void) makeUpgradeRequest:(NSURLRequest*) request
{
// reset our response data
[upgradeResponseData setLength:0];
if(!request)
NSLog(@"its bloody nil");
upgradeConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
+ (void) checkForUpdate:(NSString*) appName
{
NSString* appVersion = (NSString*)[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
if(appVersion == nil)
return;
Reachability* reachability = [Reachability reachabilityForInternetConnection];
NetworkStatus remoteStatus = [reachability currentReachabilityStatus];
if(remoteStatus == NotReachable)
{
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"ConnectionError", nil)];
return;
}
appVersion = [NSString stringWithFormat:@"%s%@",RBX::Http::urlEncode([appName cStringUsingEncoding:NSUTF8StringEncoding]).c_str(),appVersion];
NSString *urlString = [NSString stringWithFormat:[UpgradeCheckHelper getUpgradeUrl],appVersion];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[[UpgradeCheckHelper getUpgradeCheckHelper] makeUpgradeRequest:request];
}
- (NSString*) getLocalizedUpgradeString:(NSString*) localizedKey
{
NSString* upgradeString = [NSString stringWithFormat:NSLocalizedString(localizedKey, nil), @"ROBLOX"];
id bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
if (bundleName)
{
if ([bundleName isKindOfClass:[NSString class]])
{
NSString* bundleString = (NSString*)bundleName;
upgradeString = [NSString stringWithFormat:NSLocalizedString(localizedKey, nil), bundleString];
}
}
return upgradeString;
}
- (void) processCheckForUpdateResponse
{
// grab info from connection
NSError *e = nil;
NSDictionary *jsonDictionary = [NSJSONSerialization JSONObjectWithData: upgradeResponseData options: NSJSONReadingAllowFragments error: &e];
if (jsonDictionary == nil || e)
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "Error parsing JSON in AppDelegate: %s",[[e localizedDescription] cStringUsingEncoding:NSUTF8StringEncoding]);
return;
}
// parse thru the dictionaries to get the proper responses
NSDictionary* responseDictionary;
if([jsonDictionary objectForKey:@"data"] != [NSNull null])
responseDictionary = (NSDictionary*)[jsonDictionary valueForKey:@"data"];
else
return;
NSString* upgradeAction = nil;
if([responseDictionary objectForKey:@"UpgradeAction"] != [NSNull null])
upgradeAction = [responseDictionary objectForKey:@"UpgradeAction"];
else
return;
NSString* upgradeString = nil;
if([responseDictionary objectForKey:@"Message"] != [NSNull null])
upgradeString = [responseDictionary objectForKey:@"Message"];
// post message to user
if ( [upgradeAction isEqualToString:@"Recommended"] == TRUE )
{
//[[PlaceLauncher sharedInstance] leaveGame];
if (upgradeString == nil)
{
upgradeString = [self getLocalizedUpgradeString:@"RecommendUpgradeBody"];
}
upgradeAlertView.title = NSLocalizedString(@"RecommendUpgradeTitle", nil);
upgradeAlertView.message = upgradeString;
BOOL addIgnoreButton = YES;
UIButton* button = [self getAlertViewButton:NSLocalizedString(@"IgnoreButtonText", nil)];
if(button != nil)
addIgnoreButton = NO;
if(addIgnoreButton)
[upgradeAlertView addButtonWithTitle:NSLocalizedString(@"IgnoreButtonText", nil)];
[upgradeAlertView addButtonWithTitle:NSLocalizedString(@"UpgradeButtonText", nil)];
button = [self getAlertViewButton:NSLocalizedString(@"UpgradeButtonText", nil)];
if(button != nil)
{
button.enabled = YES;
button.highlighted = YES;
}
dispatch_async(dispatch_get_main_queue(),^{
[upgradeAlertView show];
});
}
else if ( [upgradeAction isEqualToString:@"Required"] == TRUE )
{
//[[PlaceLauncher sharedInstance] leaveGame];
if (upgradeString == nil)
{
upgradeString = [self getLocalizedUpgradeString:@"RequireUpgradeBody"];
}
upgradeAlertView.title = NSLocalizedString(@"RequireUpgradeTitle", nil);
upgradeAlertView.message = upgradeString;
[upgradeAlertView addButtonWithTitle:NSLocalizedString(@"UpgradeButtonText", nil)];
dispatch_async(dispatch_get_main_queue(),^{
[upgradeAlertView show];
});
}
}
// NSURLConnectionDataDelegate methods
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
if(connection == upgradeConnection)
[upgradeResponseData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if(connection == upgradeConnection)
{
upgradeConnection = nil;
[self processCheckForUpdateResponse];
}
}
// UIAlertViewDelegate methods
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if ( upgradeAlertView == alertView)
{
if ([[alertView buttonTitleAtIndex:buttonIndex] isEqualToString:NSLocalizedString(@"UpgradeButtonText", nil)])
{
id appID = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"AppiraterId"];
if(appID && [appID isKindOfClass:[NSString class]])
{
NSString* url = [NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@", appID];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]];
}
else
{
id bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
if (bundleName)
{
if ([bundleName isKindOfClass:[NSString class]])
{
NSString* bundleString = (NSString*)bundleName;
bundleString = [bundleString stringByReplacingOccurrencesOfString:@" " withString:@""];
// bundle name "ROBLOX" does not correspond to app store url, needs mobile
if ([bundleString isEqualToString:@"ROBLOX"])
bundleString = [bundleString stringByAppendingString:@"mobile"];
NSString* url = [@"itms-apps://itunes.com/apps/" stringByAppendingString:bundleString];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]];
}
}
}
}
}
}
@end