You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1197 lines
42 KiB

//
// iRate.m
//
// Version 1.11.4
//
// Created by Nick Lockwood on 26/01/2011.
// Copyright 2011 Charcoal Design
//
// Distributed under the permissive zlib license
// Get the latest version from here:
//
// https://github.com/nicklockwood/iRate
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
#import "iRate.h"
#import "PanelController.h"
#import "CommonStrings.h"
#import <Availability.h>
#if !__has_feature(objc_arc)
#error This class requires automatic reference counting
#endif
#pragma clang diagnostic ignored "-Wreceiver-is-weak"
#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak"
#pragma clang diagnostic ignored "-Wobjc-missing-property-synthesis"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Wunused-macros"
#pragma clang diagnostic ignored "-Wconversion"
#pragma clang diagnostic ignored "-Wformat-nonliteral"
#pragma clang diagnostic ignored "-Wselector"
#pragma clang diagnostic ignored "-Wgnu"
NSUInteger const iRateAppStoreGameGenreID = 6014;
NSString *const iRateErrorDomain = @"iRateErrorDomain";
NSString *const iRateMessageTitleKey = @"iRateMessageTitle";
NSString *const iRateAppMessageKey = @"iRateAppMessage";
NSString *const iRateGameMessageKey = @"iRateGameMessage";
NSString *const iRateUpdateMessageKey = @"iRateUpdateMessage";
NSString *const iRateCancelButtonKey = @"iRateCancelButton";
NSString *const iRateRemindButtonKey = @"iRateRemindButton";
NSString *const iRateRateButtonKey = @"iRateRateButton";
NSString *const iRateCouldNotConnectToAppStore = @"iRateCouldNotConnectToAppStore";
NSString *const iRateDidDetectAppUpdate = @"iRateDidDetectAppUpdate";
NSString *const iRateDidPromptForRating = @"iRateDidPromptForRating";
NSString *const iRateUserDidAttemptToRateApp = @"iRateUserDidAttemptToRateApp";
NSString *const iRateUserDidDeclineToRateApp = @"iRateUserDidDeclineToRateApp";
NSString *const iRateUserDidRequestReminderToRateApp = @"iRateUserDidRequestReminderToRateApp";
NSString *const iRateDidOpenAppStore = @"iRateDidOpenAppStore";
static NSString *const iRateAppStoreIDKey = @"iRateAppStoreID";
static NSString *const iRateRatedVersionKey = @"iRateRatedVersionChecked";
static NSString *const iRateDeclinedVersionKey = @"iRateDeclinedVersion";
static NSString *const iRateLastRemindedKey = @"iRateLastReminded";
static NSString *const iRateLastVersionUsedKey = @"iRateLastVersionUsed";
static NSString *const iRateFirstUsedKey = @"iRateFirstUsed";
static NSString *const iRateUseCountKey = @"iRateUseCount";
static NSString *const iRateEventCountKey = @"iRateEventCount";
static NSString *const iRateMacAppStoreBundleID = @"com.apple.appstore";
static NSString *const iRateAppLookupURLFormat = @"https://itunes.apple.com/%@/lookup";
static NSString *const iRateiOSAppStoreURLScheme = @"itms-apps";
static NSString *const iRateiOSAppStoreURLFormat = @"itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?type=Purple+Software&id=%@&pageNumber=0&sortOrdering=2&mt=8";
static NSString *const iRateiOS7AppStoreURLFormat = @"itms-apps://itunes.apple.com/app/id%@";
static NSString *const iRateMacAppStoreURLFormat = @"macappstore://itunes.apple.com/app/id%@";
#define SECONDS_IN_A_DAY 86400.0
#define SECONDS_IN_A_WEEK 604800.0
#define MAC_APP_STORE_REFRESH_DELAY 5.0
#define REQUEST_TIMEOUT 60.0
@implementation NSObject (iRate)
- (void)iRateCouldNotConnectToAppStore:(__unused NSError *)error {}
- (void)iRateDidDetectAppUpdate {}
- (BOOL)iRateShouldPromptForRating { return YES; }
- (void)iRateDidPromptForRating {}
- (void)iRateUserDidAttemptToRateApp {}
- (void)iRateUserDidDeclineToRateApp {}
- (void)iRateUserDidRequestReminderToRateApp {}
- (BOOL)iRateShouldOpenAppStore { return YES; }
- (void)iRateDidOpenAppStore {}
@end
@interface iRate()
@property (nonatomic, strong) id visibleAlert;
@property (nonatomic, assign) BOOL checkingForPrompt;
@property (nonatomic, assign) BOOL checkingForAppStoreID;
@end
@implementation iRate
+ (void)load
{
[self performSelectorOnMainThread:@selector(sharedInstance) withObject:nil waitUntilDone:NO];
}
+ (instancetype)sharedInstance
{
static iRate *sharedInstance = nil;
if (sharedInstance == nil)
{
sharedInstance = [(iRate *)[self alloc] init];
}
return sharedInstance;
}
- (NSString *)localizedStringForKey:(NSString *)key withDefault:(NSString *)defaultString
{
static NSBundle *bundle = nil;
if (bundle == nil)
{
NSString *bundlePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"iRate" ofType:@"bundle"];
if (self.useAllAvailableLanguages)
{
bundle = [NSBundle bundleWithPath:bundlePath];
NSString *language = [[NSLocale preferredLanguages] count]? [NSLocale preferredLanguages][0]: @"en";
if (![[bundle localizations] containsObject:language])
{
language = [language componentsSeparatedByString:@"-"][0];
}
if ([[bundle localizations] containsObject:language])
{
bundlePath = [bundle pathForResource:language ofType:@"lproj"];
}
}
bundle = [NSBundle bundleWithPath:bundlePath] ?: [NSBundle mainBundle];
}
defaultString = [bundle localizedStringForKey:key value:defaultString table:nil];
return [[NSBundle mainBundle] localizedStringForKey:key value:defaultString table:nil];
}
- (iRate *)init
{
if ((self = [super init]))
{
#if TARGET_OS_IPHONE
//register for iphone application events
if (&UIApplicationWillEnterForegroundNotification)
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillEnterForeground)
name:UIApplicationWillEnterForegroundNotification
object:nil];
}
#endif
//get country
self.appStoreCountry = [(NSLocale *)[NSLocale currentLocale] objectForKey:NSLocaleCountryCode];
if ([self.appStoreCountry isEqualToString:@"150"])
{
self.appStoreCountry = @"eu";
}
else if (!self.appStoreCountry || [[self.appStoreCountry stringByReplacingOccurrencesOfString:@"[A-Za-z]{2}" withString:CLEmptyString options:NSRegularExpressionSearch range:NSMakeRange(0, 2)] length])
{
self.appStoreCountry = @"us";
}
//application version (use short version preferentially)
self.applicationVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
if ([self.applicationVersion length] == 0)
{
self.applicationVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
}
//localised application name
self.applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
if ([self.applicationName length] == 0)
{
self.applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey];
}
//bundle id
self.applicationBundleID = [[NSBundle mainBundle] bundleIdentifier];
//default settings
self.useAllAvailableLanguages = YES;
self.promptForNewVersionIfUserRated = NO;
self.onlyPromptIfLatestVersion = YES;
self.onlyPromptIfMainWindowIsAvailable = YES;
self.promptAtLaunch = YES;
self.usesUntilPrompt = 10;
self.eventsUntilPrompt = 10;
self.daysUntilPrompt = 10.0f;
self.usesPerWeekForPrompt = 0.0f;
self.remindPeriod = 1.0f;
self.verboseLogging = NO;
self.previewMode = NO;
#if DEBUG
//enable verbose logging in debug mode
self.verboseLogging = YES;
NSLog(@"iRate verbose logging enabled.");
#endif
//app launched
[self performSelectorOnMainThread:@selector(applicationLaunched) withObject:nil waitUntilDone:NO];
}
return self;
}
- (id<iRateDelegate>)delegate
{
if (_delegate == nil)
{
#if TARGET_OS_IPHONE
#define APP_CLASS UIApplication
#else
#define APP_CLASS NSApplication
#endif
_delegate = (id<iRateDelegate>)[[APP_CLASS sharedApplication] delegate];
}
return _delegate;
}
- (NSString *)messageTitle
{
return [_messageTitle ?: [self localizedStringForKey:iRateMessageTitleKey withDefault:@"Rate %@"] stringByReplacingOccurrencesOfString:@"%@" withString:self.applicationName];
}
- (NSString *)message
{
NSString *message = _message;
if (!message)
{
message = (self.appStoreGenreID == iRateAppStoreGameGenreID)? [self localizedStringForKey:iRateGameMessageKey withDefault:@"If you enjoy playing %@, would you mind taking a moment to rate it? It won’t take more than a minute. Thanks for your support!"]: [self localizedStringForKey:iRateAppMessageKey withDefault:@"If you enjoy using %@, would you mind taking a moment to rate it? It won’t take more than a minute. Thanks for your support!"];
}
return [message stringByReplacingOccurrencesOfString:@"%@" withString:self.applicationName];
}
- (NSString *)updateMessage
{
NSString *updateMessage = _updateMessage;
if (!updateMessage)
{
updateMessage = [self localizedStringForKey:iRateUpdateMessageKey withDefault:self.message];
}
return [updateMessage stringByReplacingOccurrencesOfString:@"%@" withString:self.applicationName];
}
- (NSString *)cancelButtonLabel
{
return _cancelButtonLabel ?: [self localizedStringForKey:iRateCancelButtonKey withDefault:@"No, Thanks"];
}
- (NSString *)rateButtonLabel
{
return _rateButtonLabel ?: [self localizedStringForKey:iRateRateButtonKey withDefault:@"Rate It Now"];
}
- (NSString *)remindButtonLabel
{
return _remindButtonLabel ?: [self localizedStringForKey:iRateRemindButtonKey withDefault:@"Remind Me Later"];
}
- (NSURL *)ratingsURL
{
if (_ratingsURL)
{
return _ratingsURL;
}
if (!self.appStoreID && self.verboseLogging)
{
NSLog(@"iRate could not find the App Store ID for this application. If the application is not intended for App Store release then you must specify a custom ratingsURL.");
}
NSString *URLString;
#if TARGET_OS_IPHONE
float iOSVersion = [[UIDevice currentDevice].systemVersion floatValue];
if (iOSVersion >= 7.0f && iOSVersion < 7.1f)
{
URLString = iRateiOS7AppStoreURLFormat;
}
else
{
URLString = iRateiOSAppStoreURLFormat;
}
#else
URLString = iRateMacAppStoreURLFormat;
#endif
return [NSURL URLWithString:[NSString stringWithFormat:URLString, @(self.appStoreID)]];
}
- (NSUInteger)appStoreID
{
return _appStoreID ?: [[[NSUserDefaults standardUserDefaults] objectForKey:iRateAppStoreIDKey] unsignedIntegerValue];
}
- (NSDate *)firstUsed
{
return [[NSUserDefaults standardUserDefaults] objectForKey:iRateFirstUsedKey];
}
- (void)setFirstUsed:(NSDate *)date
{
[[NSUserDefaults standardUserDefaults] setObject:date forKey:iRateFirstUsedKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSDate *)lastReminded
{
return [[NSUserDefaults standardUserDefaults] objectForKey:iRateLastRemindedKey];
}
- (void)setLastReminded:(NSDate *)date
{
[[NSUserDefaults standardUserDefaults] setObject:date forKey:iRateLastRemindedKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSUInteger)usesCount
{
return [[NSUserDefaults standardUserDefaults] integerForKey:iRateUseCountKey];
}
- (void)setUsesCount:(NSUInteger)count
{
[[NSUserDefaults standardUserDefaults] setInteger:(NSInteger)count forKey:iRateUseCountKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSUInteger)eventCount
{
return [[NSUserDefaults standardUserDefaults] integerForKey:iRateEventCountKey];
}
- (void)setEventCount:(NSUInteger)count
{
[[NSUserDefaults standardUserDefaults] setInteger:(NSInteger)count forKey:iRateEventCountKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (float)usesPerWeek
{
return (float)self.usesCount / ([[NSDate date] timeIntervalSinceDate:self.firstUsed] / SECONDS_IN_A_WEEK);
}
- (BOOL)declinedThisVersion
{
return [[[NSUserDefaults standardUserDefaults] objectForKey:iRateDeclinedVersionKey] isEqualToString:self.applicationVersion];
}
- (void)setDeclinedThisVersion:(BOOL)declined
{
[[NSUserDefaults standardUserDefaults] setObject:(declined? self.applicationVersion: nil) forKey:iRateDeclinedVersionKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (BOOL)declinedAnyVersion
{
return [(NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:iRateDeclinedVersionKey] length] != 0;
}
- (BOOL)ratedVersion:(NSString *)version
{
return [[[NSUserDefaults standardUserDefaults] objectForKey:iRateRatedVersionKey] isEqualToString:version];
}
- (BOOL)ratedThisVersion
{
return [self ratedVersion:self.applicationVersion];
}
- (void)setRatedThisVersion:(BOOL)rated
{
[[NSUserDefaults standardUserDefaults] setObject:(rated? self.applicationVersion: nil) forKey:iRateRatedVersionKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (BOOL)ratedAnyVersion
{
return [(NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:iRateRatedVersionKey] length] != 0;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)incrementUseCount
{
self.usesCount ++;
}
- (void)incrementEventCount
{
self.eventCount ++;
}
- (BOOL)shouldPromptForRating
{
//preview mode?
if (self.previewMode)
{
NSLog(@"iRate preview mode is enabled - make sure you disable this for release");
return YES;
}
//check if we've rated this version
else if (self.ratedThisVersion)
{
if (self.verboseLogging)
{
NSLog(@"iRate did not prompt for rating because the user has already rated this version");
}
return NO;
}
//check if we've rated any version
else if (self.ratedAnyVersion && !self.promptForNewVersionIfUserRated)
{
if (self.verboseLogging)
{
NSLog(@"iRate did not prompt for rating because the user has already rated this app, and promptForNewVersionIfUserRated is disabled");
}
return NO;
}
//check if we've declined to rate the app
else if (self.declinedAnyVersion)
{
if (self.verboseLogging)
{
NSLog(@"iRate did not prompt for rating because the user has declined to rate the app");
}
return NO;
}
//check how long we've been using this version
else if ([[NSDate date] timeIntervalSinceDate:self.firstUsed] < self.daysUntilPrompt * SECONDS_IN_A_DAY)
{
if (self.verboseLogging)
{
NSLog(@"iRate did not prompt for rating because the app was first used less than %g days ago", self.daysUntilPrompt);
}
return NO;
}
//check how many times we've used it and the number of significant events
else if (self.usesCount < self.usesUntilPrompt && self.eventCount < self.eventsUntilPrompt)
{
if (self.verboseLogging)
{
NSLog(@"iRate did not prompt for rating because the app has only been used %@ times and only %@ events have been logged", @(self.usesCount), @(self.eventCount));
}
return NO;
}
//check if usage frequency is high enough
else if (self.usesPerWeek < self.usesPerWeekForPrompt)
{
if (self.verboseLogging)
{
NSLog(@"iRate did not prompt for rating because the app has only been used %g times per week on average since it was installed", self.usesPerWeek);
}
return NO;
}
//check if within the reminder period
else if (self.lastReminded != nil && [[NSDate date] timeIntervalSinceDate:self.lastReminded] < self.remindPeriod * SECONDS_IN_A_DAY)
{
if (self.verboseLogging)
{
NSLog(@"iRate did not prompt for rating because the user last asked to be reminded less than %g days ago", self.remindPeriod);
}
return NO;
}
//lets prompt!
return YES;
}
- (NSString *)valueForKey:(NSString *)key inJSON:(id)json
{
if ([json isKindOfClass:[NSString class]])
{
//use legacy parser
NSRange keyRange = [json rangeOfString:[NSString stringWithFormat:@"\"%@\"", key]];
if (keyRange.location != NSNotFound)
{
NSInteger start = keyRange.location + keyRange.length;
NSRange valueStart = [json rangeOfString:@":" options:(NSStringCompareOptions)0 range:NSMakeRange(start, [(NSString *)json length] - start)];
if (valueStart.location != NSNotFound)
{
start = valueStart.location + 1;
NSRange valueEnd = [json rangeOfString:@"," options:(NSStringCompareOptions)0 range:NSMakeRange(start, [(NSString *)json length] - start)];
if (valueEnd.location != NSNotFound)
{
NSString *value = [json substringWithRange:NSMakeRange(start, valueEnd.location - start)];
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
while ([value hasPrefix:@"\""] && ![value hasSuffix:@"\""])
{
if (valueEnd.location == NSNotFound)
{
break;
}
NSInteger newStart = valueEnd.location + 1;
valueEnd = [json rangeOfString:@"," options:(NSStringCompareOptions)0 range:NSMakeRange(newStart, [(NSString *)json length] - newStart)];
value = [json substringWithRange:NSMakeRange(start, valueEnd.location - start)];
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\""]];
value = [value stringByReplacingOccurrencesOfString:@"\\\\" withString:@"\\"];
value = [value stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
value = [value stringByReplacingOccurrencesOfString:@"\\\"" withString:@"\""];
value = [value stringByReplacingOccurrencesOfString:@"\\n" withString:@"\n"];
value = [value stringByReplacingOccurrencesOfString:@"\\r" withString:@"\r"];
value = [value stringByReplacingOccurrencesOfString:@"\\t" withString:@"\t"];
value = [value stringByReplacingOccurrencesOfString:@"\\f" withString:@"\f"];
value = [value stringByReplacingOccurrencesOfString:@"\\b" withString:@"\f"];
while (YES)
{
NSRange unicode = [value rangeOfString:@"\\u"];
if (unicode.location == NSNotFound || unicode.location + unicode.length == 0)
{
break;
}
uint32_t c = 0;
NSString *hex = [value substringWithRange:NSMakeRange(unicode.location + 2, 4)];
NSScanner *scanner = [NSScanner scannerWithString:hex];
[scanner scanHexInt:&c];
if (c <= 0xffff)
{
value = [value stringByReplacingCharactersInRange:NSMakeRange(unicode.location, 6) withString:[NSString stringWithFormat:@"%C", (unichar)c]];
}
else
{
//convert character to surrogate pair
uint16_t x = (uint16_t)c;
uint16_t u = (c >> 16) & ((1 << 5) - 1);
uint16_t w = (uint16_t)u - 1;
unichar high = 0xd800 | (w << 6) | x >> 10;
unichar low = (uint16_t)(0xdc00 | (x & ((1 << 10) - 1)));
value = [value stringByReplacingCharactersInRange:NSMakeRange(unicode.location, 6) withString:[NSString stringWithFormat:@"%C%C", high, low]];
}
}
return value;
}
}
}
}
else
{
return json[key];
}
return nil;
}
- (void)setAppStoreIDOnMainThread:(NSString *)appStoreIDString
{
_appStoreID = [appStoreIDString integerValue];
[[NSUserDefaults standardUserDefaults] setInteger:_appStoreID forKey:iRateAppStoreIDKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)connectionSucceeded
{
if (self.checkingForAppStoreID)
{
//no longer checking
self.checkingForPrompt = NO;
self.checkingForAppStoreID = NO;
//open app store
[self openRatingsPageInAppStore];
}
else if (self.checkingForPrompt)
{
//no longer checking
self.checkingForPrompt = NO;
//confirm with delegate
if (![self.delegate iRateShouldPromptForRating])
{
if (self.verboseLogging)
{
NSLog(@"iRate did not display the rating prompt because the iRateShouldPromptForRating delegate method returned NO");
}
return;
}
//prompt user
[self promptForRating];
}
}
- (void)connectionError:(NSError *)error
{
if (self.checkingForPrompt || self.checkingForAppStoreID)
{
//no longer checking
self.checkingForPrompt = NO;
self.checkingForAppStoreID = NO;
//log the error
if (error)
{
NSLog(@"iRate rating process failed because: %@", [error localizedDescription]);
}
else
{
NSLog(@"iRate rating process failed because an unknown error occured");
}
//could not connect
[self.delegate iRateCouldNotConnectToAppStore:error];
[[NSNotificationCenter defaultCenter] postNotificationName:iRateCouldNotConnectToAppStore
object:error];
}
}
- (void)checkForConnectivityInBackground
{
if ([NSThread isMainThread])
{
[self performSelectorInBackground:@selector(checkForConnectivityInBackground) withObject:nil];
return;
}
@autoreleasepool
{
//prevent concurrent checks
static BOOL checking = NO;
if (checking) return;
checking = YES;
//first check iTunes
NSString *iTunesServiceURL = [NSString stringWithFormat:iRateAppLookupURLFormat, self.appStoreCountry];
if (_appStoreID) //important that we check ivar and not getter in case it has changed
{
iTunesServiceURL = [iTunesServiceURL stringByAppendingFormat:@"?id=%@", @(_appStoreID)];
}
else
{
iTunesServiceURL = [iTunesServiceURL stringByAppendingFormat:@"?bundleId=%@", self.applicationBundleID];
}
if (self.verboseLogging)
{
NSLog(@"iRate is checking %@ to retrieve the App Store details...", iTunesServiceURL);
}
NSError *error = nil;
NSURLResponse *response = nil;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:iTunesServiceURL] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:REQUEST_TIMEOUT];
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
if (data && statusCode == 200)
{
//in case error is garbage...
error = nil;
id json = nil;
if ([NSJSONSerialization class])
{
json = [[NSJSONSerialization JSONObjectWithData:data options:(NSJSONReadingOptions)0 error:&error][@"results"] lastObject];
}
else
{
//convert to string
json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
if (!error)
{
//check bundle ID matches
NSString *bundleID = [self valueForKey:@"bundleId" inJSON:json];
if (bundleID)
{
if ([bundleID isEqualToString:self.applicationBundleID])
{
//get genre
if (self.appStoreGenreID == 0)
{
self.appStoreGenreID = [[self valueForKey:@"primaryGenreId" inJSON:json] integerValue];
}
//get app id
if (!_appStoreID)
{
NSString *appStoreIDString = [self valueForKey:@"trackId" inJSON:json];
[self performSelectorOnMainThread:@selector(setAppStoreIDOnMainThread:) withObject:appStoreIDString waitUntilDone:YES];
if (self.verboseLogging)
{
NSLog(@"iRate found the app on iTunes. The App Store ID is %@", appStoreIDString);
}
}
//check version
if (self.onlyPromptIfLatestVersion && !self.previewMode)
{
NSString *latestVersion = [self valueForKey:@"version" inJSON:json];
if ([latestVersion compare:self.applicationVersion options:NSNumericSearch] == NSOrderedDescending)
{
if (self.verboseLogging)
{
NSLog(@"iRate found that the installed application version (%@) is not the latest version on the App Store, which is %@", self.applicationVersion, latestVersion);
}
error = [NSError errorWithDomain:iRateErrorDomain code:iRateErrorApplicationIsNotLatestVersion userInfo:@{NSLocalizedDescriptionKey: @"Installed app is not the latest version available"}];
}
}
}
else
{
if (self.verboseLogging)
{
NSLog(@"iRate found that the application bundle ID (%@) does not match the bundle ID of the app found on iTunes (%@) with the specified App Store ID (%@)", self.applicationBundleID, bundleID, @(self.appStoreID));
}
error = [NSError errorWithDomain:iRateErrorDomain code:iRateErrorBundleIdDoesNotMatchAppStore userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Application bundle ID does not match expected value of %@", bundleID]}];
}
}
else if (_appStoreID || !self.ratingsURL)
{
if (self.verboseLogging)
{
NSLog(@"iRate could not find this application on iTunes. If your app is not intended for App Store release then you must specify a custom ratingsURL. If this is the first release of your application then it's not a problem that it cannot be found on the store yet");
}
if (!self.previewMode)
{
error = [NSError errorWithDomain:iRateErrorDomain
code:iRateErrorApplicationNotFoundOnAppStore
userInfo:@{NSLocalizedDescriptionKey: @"The application could not be found on the App Store."}];
}
}
else if (!_appStoreID && self.verboseLogging)
{
NSLog(@"iRate could not find your app on iTunes. If your app is not yet on the store or is not intended for App Store release then don't worry about this");
}
}
}
else if (statusCode >= 400)
{
//http error
NSString *message = [NSString stringWithFormat:@"The server returned a %@ error", @(statusCode)];
error = [NSError errorWithDomain:@"HTTPResponseErrorDomain" code:statusCode userInfo:@{NSLocalizedDescriptionKey: message}];
}
//handle errors (ignoring sandbox issues)
if (error && !(error.code == EPERM && [error.domain isEqualToString:NSPOSIXErrorDomain] && _appStoreID))
{
[self performSelectorOnMainThread:@selector(connectionError:) withObject:error waitUntilDone:YES];
}
else if (self.appStoreID || self.previewMode)
{
//show prompt
[self performSelectorOnMainThread:@selector(connectionSucceeded) withObject:nil waitUntilDone:YES];
}
//finished
checking = NO;
}
}
- (void)promptIfNetworkAvailable
{
if (!self.checkingForPrompt && !self.checkingForAppStoreID)
{
self.checkingForPrompt = YES;
[self checkForConnectivityInBackground];
}
}
- (void)promptIfAllCriteriaMet
{
if ([self shouldPromptForRating])
{
[self promptIfNetworkAvailable];
}
}
- (BOOL)showRemindButton
{
return [self.remindButtonLabel length];
}
- (BOOL)showCancelButton
{
return [self.cancelButtonLabel length];
}
- (void)promptForRating
{
if (!self.visibleAlert)
{
NSString *message = self.ratedAnyVersion? self.updateMessage: self.message;
#if TARGET_OS_IPHONE
UIViewController *topController = [UIApplication sharedApplication].delegate.window.rootViewController;
while (topController.presentedViewController)
{
topController = topController.presentedViewController;
}
if ([UIAlertController class] && topController && self.useUIAlertControllerIfAvailable)
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:self.messageTitle message:message preferredStyle:UIAlertControllerStyleAlert];
//rate action
[alert addAction:[UIAlertAction actionWithTitle:self.rateButtonLabel style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) {
[self didDismissAlert:alert withButtonAtIndex:0];
}]];
//cancel action
if ([self showCancelButton])
{
[alert addAction:[UIAlertAction actionWithTitle:self.cancelButtonLabel style:UIAlertActionStyleCancel handler:^(__unused UIAlertAction *action) {
[self didDismissAlert:alert withButtonAtIndex:1];
}]];
}
//remind action
if ([self showRemindButton])
{
[alert addAction:[UIAlertAction actionWithTitle:self.remindButtonLabel style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) {
[self didDismissAlert:alert withButtonAtIndex:[self showCancelButton]? 2: 1];
}]];
}
self.visibleAlert = alert;
//get current view controller and present alert
[topController presentViewController:alert animated:YES completion:NULL];
}
else
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:self.messageTitle
message:message
delegate:(id<UIAlertViewDelegate>)self
cancelButtonTitle:nil
otherButtonTitles:self.rateButtonLabel, nil];
if ([self showCancelButton])
{
[alert addButtonWithTitle:self.cancelButtonLabel];
alert.cancelButtonIndex = 1;
}
if ([self showRemindButton])
{
[alert addButtonWithTitle:self.remindButtonLabel];
}
self.visibleAlert = alert;
[self.visibleAlert show];
}
#else
//only show when main window is available
if (self.onlyPromptIfMainWindowIsAvailable && ![[NSApplication sharedApplication] mainWindow])
{
[self performSelector:@selector(promptForRating) withObject:nil afterDelay:0.5];
return;
}
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = self.messageTitle;
alert.informativeText = message;
[alert addButtonWithTitle:self.rateButtonLabel];
if ([self showCancelButton])
{
[alert addButtonWithTitle:self.cancelButtonLabel];
}
if ([self showRemindButton])
{
[alert addButtonWithTitle:self.remindButtonLabel];
}
self.visibleAlert = alert;
#if __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9
if (![alert respondsToSelector:@selector(beginSheetModalForWindow:completionHandler:)])
{
[alert beginSheetModalForWindow:[NSApplication sharedApplication].mainWindow
modalDelegate:self
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
contextInfo:nil];
}
else
#endif
{
NSArray *currentWindows = [NSApplication sharedApplication].windows;
for (NSWindow *window in currentWindows)
{
if ([window.windowController isKindOfClass:[PanelController class]])
{
PanelController *panelRef = (PanelController *)[[[NSApplication sharedApplication]
mainWindow] windowController];
panelRef.showReviewCell = YES;
[panelRef updateDefaultPreferences];
}
}
}
#endif
//inform about prompt
[self.delegate iRateDidPromptForRating];
[[NSNotificationCenter defaultCenter] postNotificationName:iRateDidPromptForRating
object:nil];
}
}
- (void)applicationLaunched
{
//check if this is a new version
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *lastUsedVersion = [defaults objectForKey:iRateLastVersionUsedKey];
if (!self.firstUsed || ![lastUsedVersion isEqualToString:self.applicationVersion])
{
[defaults setObject:self.applicationVersion forKey:iRateLastVersionUsedKey];
if (!self.firstUsed || [self ratedAnyVersion])
{
//reset defaults
[defaults setObject:[NSDate date] forKey:iRateFirstUsedKey];
[defaults setInteger:0 forKey:iRateUseCountKey];
[defaults setInteger:0 forKey:iRateEventCountKey];
[defaults setObject:nil forKey:iRateLastRemindedKey];
[defaults synchronize];
}
else if ([[NSDate date] timeIntervalSinceDate:self.firstUsed] > (self.daysUntilPrompt - 1) * SECONDS_IN_A_DAY)
{
//if was previously installed, but we haven't yet prompted for a rating
//don't reset, but make sure it won't rate for a day at least
self.firstUsed = [[NSDate date] dateByAddingTimeInterval:(self.daysUntilPrompt - 1) * -SECONDS_IN_A_DAY];
}
//inform about app update
[self.delegate iRateDidDetectAppUpdate];
[[NSNotificationCenter defaultCenter] postNotificationName:iRateDidDetectAppUpdate
object:nil];
}
[self incrementUseCount];
[self checkForConnectivityInBackground];
if (self.promptAtLaunch)
{
[self promptIfAllCriteriaMet];
}
}
- (void)didDismissAlert:(__unused id)alertView withButtonAtIndex:(NSInteger)buttonIndex
{
//get button indices
NSInteger rateButtonIndex = 0;
NSInteger cancelButtonIndex = [self showCancelButton]? 1: 0;
NSInteger remindButtonIndex = [self showRemindButton]? cancelButtonIndex + 1: 0;
if (buttonIndex == rateButtonIndex)
{
[self rate];
}
else if (buttonIndex == cancelButtonIndex)
{
[self declineThisVersion];
}
else if (buttonIndex == remindButtonIndex)
{
[self remindLater];
}
//release alert
self.visibleAlert = nil;
}
#if TARGET_OS_IPHONE
- (void)applicationWillEnterForeground
{
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground)
{
[self incrementUseCount];
[self checkForConnectivityInBackground];
if (self.promptAtLaunch)
{
[self promptIfAllCriteriaMet];
}
}
}
- (void)openRatingsPageInAppStore
{
if (!_ratingsURL && !self.appStoreID)
{
self.checkingForAppStoreID = YES;
if (!self.checkingForPrompt)
{
[self checkForConnectivityInBackground];
}
return;
}
NSString *cantOpenMessage = nil;
#if TARGET_IPHONE_SIMULATOR
if ([[self.ratingsURL scheme] isEqualToString:iRateiOSAppStoreURLScheme])
{
cantOpenMessage = @"iRate could not open the ratings page because the App Store is not available on the iOS simulator";
}
#elif DEBUG
if (![[UIApplication sharedApplication] canOpenURL:self.ratingsURL])
{
cantOpenMessage = [NSString stringWithFormat:@"iRate was unable to open the specified ratings URL: %@", self.ratingsURL];
}
#endif
if (cantOpenMessage)
{
NSLog(@"%@", cantOpenMessage);
NSError *error = [NSError errorWithDomain:iRateErrorDomain code:iRateErrorCouldNotOpenRatingPageURL userInfo:@{NSLocalizedDescriptionKey: cantOpenMessage}];
[self.delegate iRateCouldNotConnectToAppStore:error];
[[NSNotificationCenter defaultCenter] postNotificationName:iRateCouldNotConnectToAppStore
object:error];
}
else
{
if (self.verboseLogging)
{
NSLog(@"iRate will open the App Store ratings page using the following URL: %@", self.ratingsURL);
}
[[UIApplication sharedApplication] openURL:self.ratingsURL];
[self.delegate iRateDidOpenAppStore];
[[NSNotificationCenter defaultCenter] postNotificationName:iRateDidOpenAppStore
object:nil];
}
}
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
[self didDismissAlert:alertView withButtonAtIndex:buttonIndex];
}
#else
- (void)openAppPageWhenAppStoreLaunched
{
//check if app store is running
for (NSRunningApplication *app in [[NSWorkspace sharedWorkspace] runningApplications])
{
if ([app.bundleIdentifier isEqualToString:iRateMacAppStoreBundleID])
{
//open app page
[[NSWorkspace sharedWorkspace] performSelector:@selector(openURL:) withObject:self.ratingsURL afterDelay:MAC_APP_STORE_REFRESH_DELAY];
return;
}
}
//try again
[self performSelector:@selector(openAppPageWhenAppStoreLaunched) withObject:nil afterDelay:0.0];
}
- (void)openRatingsPageInAppStore
{
if (!_ratingsURL && !self.appStoreID)
{
self.checkingForAppStoreID = YES;
if (!self.checkingForPrompt)
{
[self checkForConnectivityInBackground];
}
return;
}
if (self.verboseLogging)
{
NSLog(@"iRate will open the App Store ratings page using the following URL: %@", self.ratingsURL);
}
[[NSWorkspace sharedWorkspace] openURL:self.ratingsURL];
[self openAppPageWhenAppStoreLaunched];
[self.delegate iRateDidOpenAppStore];
}
- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(__unused void *)contextInfo
{
[self didDismissAlert:alert withButtonAtIndex:returnCode - NSAlertFirstButtonReturn];
}
#endif
- (void)logEvent:(BOOL)deferPrompt
{
[self incrementEventCount];
if (!deferPrompt)
{
[self promptIfAllCriteriaMet];
}
}
#pragma mark - User's actions
- (void)declineThisVersion
{
//ignore this version
self.declinedThisVersion = YES;
//log event
[self.delegate iRateUserDidDeclineToRateApp];
[[NSNotificationCenter defaultCenter] postNotificationName:iRateUserDidDeclineToRateApp
object:nil];
}
- (void)remindLater
{
//remind later
self.lastReminded = [NSDate date];
//log event
[self.delegate iRateUserDidRequestReminderToRateApp];
[[NSNotificationCenter defaultCenter] postNotificationName:iRateUserDidRequestReminderToRateApp
object:nil];
}
- (void)rate
{
//mark as rated
self.ratedThisVersion = YES;
//log event
[self.delegate iRateUserDidAttemptToRateApp];
[[NSNotificationCenter defaultCenter] postNotificationName:iRateUserDidAttemptToRateApp
object:nil];
if ([self.delegate iRateShouldOpenAppStore])
{
//launch mac app store
[self openRatingsPageInAppStore];
}
}
@end