iRate.m 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266
  1. //
  2. // iRate.m
  3. //
  4. // Version 1.12.1
  5. //
  6. // Created by Nick Lockwood on 26/01/2011.
  7. // Copyright 2011 Charcoal Design
  8. //
  9. // Distributed under the permissive zlib license
  10. // Get the latest version from here:
  11. //
  12. // https://github.com/nicklockwood/iRate
  13. //
  14. // This software is provided 'as-is', without any express or implied
  15. // warranty. In no event will the authors be held liable for any damages
  16. // arising from the use of this software.
  17. //
  18. // Permission is granted to anyone to use this software for any purpose,
  19. // including commercial applications, and to alter it and redistribute it
  20. // freely, subject to the following restrictions:
  21. //
  22. // 1. The origin of this software must not be misrepresented; you must not
  23. // claim that you wrote the original software. If you use this software
  24. // in a product, an acknowledgment in the product documentation would be
  25. // appreciated but is not required.
  26. //
  27. // 2. Altered source versions must be plainly marked as such, and must not be
  28. // misrepresented as being the original software.
  29. //
  30. // 3. This notice may not be removed or altered from any source distribution.
  31. //
  32. #import "iRate.h"
  33. #import <Availability.h>
  34. #if !__has_feature(objc_arc)
  35. #error This class requires automatic reference counting
  36. #endif
  37. #pragma clang diagnostic ignored "-Warc-repeated-use-of-weak"
  38. #pragma clang diagnostic ignored "-Wobjc-missing-property-synthesis"
  39. #pragma clang diagnostic ignored "-Wdirect-ivar-access"
  40. #pragma clang diagnostic ignored "-Wpartial-availability"
  41. #pragma clang diagnostic ignored "-Wunused-macros"
  42. #pragma clang diagnostic ignored "-Wconversion"
  43. #pragma clang diagnostic ignored "-Wformat-nonliteral"
  44. #pragma clang diagnostic ignored "-Wdouble-promotion"
  45. #pragma clang diagnostic ignored "-Wselector"
  46. #pragma clang diagnostic ignored "-Wgnu"
  47. NSUInteger const iRateAppStoreGameGenreID = 6014;
  48. NSString *const iRateErrorDomain = @"iRateErrorDomain";
  49. NSString *const iRateMessageTitleKey = @"iRateMessageTitle";
  50. NSString *const iRateAppMessageKey = @"iRateAppMessage";
  51. NSString *const iRateGameMessageKey = @"iRateGameMessage";
  52. NSString *const iRateUpdateMessageKey = @"iRateUpdateMessage";
  53. NSString *const iRateCancelButtonKey = @"iRateCancelButton";
  54. NSString *const iRateRemindButtonKey = @"iRateRemindButton";
  55. NSString *const iRateRateButtonKey = @"iRateRateButton";
  56. NSString *const iRateCouldNotConnectToAppStore = @"iRateCouldNotConnectToAppStore";
  57. NSString *const iRateDidDetectAppUpdate = @"iRateDidDetectAppUpdate";
  58. NSString *const iRateDidPromptForRating = @"iRateDidPromptForRating";
  59. NSString *const iRateUserDidAttemptToRateApp = @"iRateUserDidAttemptToRateApp";
  60. NSString *const iRateUserDidDeclineToRateApp = @"iRateUserDidDeclineToRateApp";
  61. NSString *const iRateUserDidRequestReminderToRateApp = @"iRateUserDidRequestReminderToRateApp";
  62. NSString *const iRateDidOpenAppStore = @"iRateDidOpenAppStore";
  63. static NSString *const iRateAppStoreIDKey = @"iRateAppStoreID";
  64. static NSString *const iRateRatedVersionKey = @"iRateRatedVersionChecked";
  65. static NSString *const iRateDeclinedVersionKey = @"iRateDeclinedVersion";
  66. static NSString *const iRateLastRemindedKey = @"iRateLastReminded";
  67. static NSString *const iRateLastVersionUsedKey = @"iRateLastVersionUsed";
  68. static NSString *const iRateFirstUsedKey = @"iRateFirstUsed";
  69. static NSString *const iRateUseCountKey = @"iRateUseCount";
  70. static NSString *const iRateEventCountKey = @"iRateEventCount";
  71. static NSString *const iRateMacAppStoreBundleID = @"com.apple.appstore";
  72. static NSString *const iRateAppLookupURLFormat = @"https://itunes.apple.com/%@/lookup";
  73. static NSString *const iRateiOSAppStoreURLScheme = @"itms-apps";
  74. static NSString *const iRateiOSAppStoreURLFormat = @"itms-apps://itunes.apple.com/app/id%@?action=write-review";
  75. static NSString *const iRateMacAppStoreURLFormat = @"macappstore://itunes.apple.com/app/id%@?action=write-review";
  76. #define SECONDS_IN_A_DAY 86400.0
  77. #define SECONDS_IN_A_WEEK 604800.0
  78. #define MAC_APP_STORE_REFRESH_DELAY 5.0
  79. #define REQUEST_TIMEOUT 60.0
  80. @interface iRate()
  81. @property (nonatomic, strong) id visibleAlert;
  82. @property (nonatomic, assign) BOOL checkingForPrompt;
  83. @property (nonatomic, assign) BOOL checkingForAppStoreID;
  84. @end
  85. @implementation iRate
  86. + (void)load
  87. {
  88. dispatch_async(dispatch_get_main_queue(), ^{
  89. [self sharedInstance];
  90. });
  91. }
  92. + (instancetype)sharedInstance
  93. {
  94. static dispatch_once_t once;
  95. static iRate *sharedInstance = nil;
  96. dispatch_once(&once, ^{
  97. sharedInstance = [(iRate *)[self alloc] init];
  98. });
  99. return sharedInstance;
  100. }
  101. - (NSString *)localizedStringForKey:(NSString *)key withDefault:(NSString *)defaultString
  102. {
  103. static NSBundle *bundle = nil;
  104. if (bundle == nil)
  105. {
  106. NSString *bundlePath = [[NSBundle bundleForClass:[iRate class]] pathForResource:@"iRate" ofType:@"bundle"];
  107. if (self.useAllAvailableLanguages)
  108. {
  109. bundle = [NSBundle bundleWithPath:bundlePath];
  110. NSString *language = [[NSLocale preferredLanguages] count]? [NSLocale preferredLanguages][0]: @"en";
  111. if (![[bundle localizations] containsObject:language])
  112. {
  113. language = [language componentsSeparatedByString:@"-"][0];
  114. }
  115. if ([[bundle localizations] containsObject:language])
  116. {
  117. bundlePath = [bundle pathForResource:language ofType:@"lproj"];
  118. }
  119. }
  120. bundle = [NSBundle bundleWithPath:bundlePath] ?: [NSBundle mainBundle];
  121. }
  122. defaultString = [bundle localizedStringForKey:key value:defaultString table:nil];
  123. return [[NSBundle mainBundle] localizedStringForKey:key value:defaultString table:nil];
  124. }
  125. - (iRate *)init
  126. {
  127. if ((self = [super init]))
  128. {
  129. #if TARGET_OS_IPHONE
  130. //register for iphone application events
  131. if (&UIApplicationWillEnterForegroundNotification)
  132. {
  133. [[NSNotificationCenter defaultCenter] addObserver:self
  134. selector:@selector(applicationWillEnterForeground)
  135. name:UIApplicationWillEnterForegroundNotification
  136. object:nil];
  137. }
  138. #endif
  139. //get country
  140. self.appStoreCountry = [(NSLocale *)[NSLocale currentLocale] objectForKey:NSLocaleCountryCode];
  141. if ([self.appStoreCountry isEqualToString:@"150"])
  142. {
  143. self.appStoreCountry = @"eu";
  144. }
  145. else if (!self.appStoreCountry || [[self.appStoreCountry stringByReplacingOccurrencesOfString:@"[A-Za-z]{2}" withString:@"" options:NSRegularExpressionSearch range:NSMakeRange(0, 2)] length])
  146. {
  147. self.appStoreCountry = @"us";
  148. }
  149. else if ([self.appStoreCountry isEqualToString:@"GI"])
  150. {
  151. self.appStoreCountry = @"GB";
  152. }
  153. //application version (use short version preferentially)
  154. self.applicationVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
  155. if ([self.applicationVersion length] == 0)
  156. {
  157. self.applicationVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey];
  158. }
  159. //localised application name
  160. self.applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
  161. if ([self.applicationName length] == 0)
  162. {
  163. self.applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleNameKey];
  164. }
  165. //bundle id
  166. self.applicationBundleID = [[NSBundle mainBundle] bundleIdentifier];
  167. //default settings
  168. self.useAllAvailableLanguages = YES;
  169. self.promptForNewVersionIfUserRated = NO;
  170. self.onlyPromptIfLatestVersion = YES;
  171. self.onlyPromptIfMainWindowIsAvailable = YES;
  172. self.promptAtLaunch = YES;
  173. self.usesUntilPrompt = 10;
  174. self.eventsUntilPrompt = 10;
  175. self.daysUntilPrompt = 10.0;
  176. self.usesPerWeekForPrompt = 0.0;
  177. self.remindPeriod = 1.0;
  178. self.verboseLogging = NO;
  179. self.previewMode = NO;
  180. self.useSKStoreReviewControllerIfAvailable = YES;
  181. #if DEBUG
  182. //enable verbose logging in debug mode
  183. self.verboseLogging = YES;
  184. NSLog(@"iRate verbose logging enabled.");
  185. #endif
  186. //app launched
  187. dispatch_async(dispatch_get_main_queue(), ^{
  188. [self applicationLaunched];
  189. });
  190. }
  191. return self;
  192. }
  193. - (id<iRateDelegate>)delegate
  194. {
  195. if (_delegate == nil)
  196. {
  197. #if TARGET_OS_IPHONE
  198. #define APP_CLASS UIApplication
  199. #else
  200. #define APP_CLASS NSApplication
  201. #endif
  202. _delegate = (id<iRateDelegate>)[(APP_CLASS *)[APP_CLASS sharedApplication] delegate];
  203. }
  204. return _delegate;
  205. }
  206. - (NSString *)messageTitle
  207. {
  208. return [_messageTitle ?: [self localizedStringForKey:iRateMessageTitleKey withDefault:@"Rate %@"] stringByReplacingOccurrencesOfString:@"%@" withString:self.applicationName];
  209. }
  210. - (NSString *)message
  211. {
  212. NSString *message = _message;
  213. if (!message)
  214. {
  215. 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!"];
  216. }
  217. return [message stringByReplacingOccurrencesOfString:@"%@" withString:self.applicationName];
  218. }
  219. - (NSString *)updateMessage
  220. {
  221. NSString *updateMessage = _updateMessage;
  222. if (!updateMessage)
  223. {
  224. updateMessage = [self localizedStringForKey:iRateUpdateMessageKey withDefault:self.message];
  225. }
  226. return [updateMessage stringByReplacingOccurrencesOfString:@"%@" withString:self.applicationName];
  227. }
  228. - (NSString *)cancelButtonLabel
  229. {
  230. return _cancelButtonLabel ?: [self localizedStringForKey:iRateCancelButtonKey withDefault:@"No, Thanks"];
  231. }
  232. - (NSString *)rateButtonLabel
  233. {
  234. return _rateButtonLabel ?: [self localizedStringForKey:iRateRateButtonKey withDefault:@"Rate It Now"];
  235. }
  236. - (NSString *)remindButtonLabel
  237. {
  238. return _remindButtonLabel ?: [self localizedStringForKey:iRateRemindButtonKey withDefault:@"Remind Me Later"];
  239. }
  240. - (NSURL *)ratingsURL
  241. {
  242. if (_ratingsURL)
  243. {
  244. return _ratingsURL;
  245. }
  246. if (!self.appStoreID && self.verboseLogging)
  247. {
  248. 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.");
  249. }
  250. NSString *URLString;
  251. #if TARGET_OS_IPHONE
  252. URLString = iRateiOSAppStoreURLFormat;
  253. #else
  254. URLString = iRateMacAppStoreURLFormat;
  255. #endif
  256. return [NSURL URLWithString:[NSString stringWithFormat:URLString, @(self.appStoreID)]];
  257. }
  258. - (NSUInteger)appStoreID
  259. {
  260. return _appStoreID ?: [[[NSUserDefaults standardUserDefaults] objectForKey:iRateAppStoreIDKey] unsignedIntegerValue];
  261. }
  262. - (NSDate *)firstUsed
  263. {
  264. return [[NSUserDefaults standardUserDefaults] objectForKey:iRateFirstUsedKey];
  265. }
  266. - (void)setFirstUsed:(NSDate *)date
  267. {
  268. [[NSUserDefaults standardUserDefaults] setObject:date forKey:iRateFirstUsedKey];
  269. [[NSUserDefaults standardUserDefaults] synchronize];
  270. }
  271. - (NSDate *)lastReminded
  272. {
  273. return [[NSUserDefaults standardUserDefaults] objectForKey:iRateLastRemindedKey];
  274. }
  275. - (void)setLastReminded:(NSDate *)date
  276. {
  277. [[NSUserDefaults standardUserDefaults] setObject:date forKey:iRateLastRemindedKey];
  278. [[NSUserDefaults standardUserDefaults] synchronize];
  279. }
  280. - (NSUInteger)usesCount
  281. {
  282. return [[NSUserDefaults standardUserDefaults] integerForKey:iRateUseCountKey];
  283. }
  284. - (void)setUsesCount:(NSUInteger)count
  285. {
  286. [[NSUserDefaults standardUserDefaults] setInteger:(NSInteger)count forKey:iRateUseCountKey];
  287. [[NSUserDefaults standardUserDefaults] synchronize];
  288. }
  289. - (NSUInteger)eventCount
  290. {
  291. return [[NSUserDefaults standardUserDefaults] integerForKey:iRateEventCountKey];
  292. }
  293. - (void)setEventCount:(NSUInteger)count
  294. {
  295. [[NSUserDefaults standardUserDefaults] setInteger:(NSInteger)count forKey:iRateEventCountKey];
  296. [[NSUserDefaults standardUserDefaults] synchronize];
  297. }
  298. - (float)usesPerWeek
  299. {
  300. return (float)self.usesCount / ([[NSDate date] timeIntervalSinceDate:self.firstUsed] / SECONDS_IN_A_WEEK);
  301. }
  302. - (BOOL)declinedThisVersion
  303. {
  304. return [[[NSUserDefaults standardUserDefaults] objectForKey:iRateDeclinedVersionKey] isEqualToString:self.applicationVersion];
  305. }
  306. - (void)setDeclinedThisVersion:(BOOL)declined
  307. {
  308. [[NSUserDefaults standardUserDefaults] setObject:(declined? self.applicationVersion: nil) forKey:iRateDeclinedVersionKey];
  309. [[NSUserDefaults standardUserDefaults] synchronize];
  310. }
  311. - (BOOL)declinedAnyVersion
  312. {
  313. return [(NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:iRateDeclinedVersionKey] length] != 0;
  314. }
  315. - (BOOL)ratedVersion:(NSString *)version
  316. {
  317. return [[[NSUserDefaults standardUserDefaults] objectForKey:iRateRatedVersionKey] isEqualToString:version];
  318. }
  319. - (BOOL)ratedThisVersion
  320. {
  321. return [self ratedVersion:self.applicationVersion];
  322. }
  323. - (void)setRatedThisVersion:(BOOL)rated
  324. {
  325. [[NSUserDefaults standardUserDefaults] setObject:(rated? self.applicationVersion: nil) forKey:iRateRatedVersionKey];
  326. [[NSUserDefaults standardUserDefaults] synchronize];
  327. }
  328. - (BOOL)ratedAnyVersion
  329. {
  330. return [(NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:iRateRatedVersionKey] length] != 0;
  331. }
  332. - (void)dealloc
  333. {
  334. [[NSNotificationCenter defaultCenter] removeObserver:self];
  335. }
  336. - (void)incrementUseCount
  337. {
  338. self.usesCount ++;
  339. }
  340. - (void)incrementEventCount
  341. {
  342. self.eventCount ++;
  343. }
  344. - (BOOL)shouldPromptForRating
  345. {
  346. //preview mode?
  347. if (self.previewMode)
  348. {
  349. NSLog(@"iRate preview mode is enabled - make sure you disable this for release");
  350. return YES;
  351. }
  352. //check if we've rated this version
  353. else if (self.ratedThisVersion)
  354. {
  355. if (self.verboseLogging)
  356. {
  357. NSLog(@"iRate did not prompt for rating because the user has already rated this version");
  358. }
  359. return NO;
  360. }
  361. //check if we've rated any version
  362. else if (self.ratedAnyVersion && !self.promptForNewVersionIfUserRated)
  363. {
  364. if (self.verboseLogging)
  365. {
  366. NSLog(@"iRate did not prompt for rating because the user has already rated this app, and promptForNewVersionIfUserRated is disabled");
  367. }
  368. return NO;
  369. }
  370. //check if we've declined to rate the app
  371. else if (self.declinedAnyVersion)
  372. {
  373. if (self.verboseLogging)
  374. {
  375. NSLog(@"iRate did not prompt for rating because the user has declined to rate the app");
  376. }
  377. return NO;
  378. }
  379. //check how long we've been using this version
  380. else if ([[NSDate date] timeIntervalSinceDate:self.firstUsed] < self.daysUntilPrompt * SECONDS_IN_A_DAY)
  381. {
  382. if (self.verboseLogging)
  383. {
  384. NSLog(@"iRate did not prompt for rating because the app was first used less than %g days ago", self.daysUntilPrompt);
  385. }
  386. return NO;
  387. }
  388. //check how many times we've used it and the number of significant events
  389. else if (self.usesCount < self.usesUntilPrompt && self.eventCount < self.eventsUntilPrompt)
  390. {
  391. if (self.verboseLogging)
  392. {
  393. 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));
  394. }
  395. return NO;
  396. }
  397. //check if usage frequency is high enough
  398. else if (self.usesPerWeek < self.usesPerWeekForPrompt)
  399. {
  400. if (self.verboseLogging)
  401. {
  402. 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);
  403. }
  404. return NO;
  405. }
  406. //check if within the reminder period
  407. else if (self.lastReminded != nil && [[NSDate date] timeIntervalSinceDate:self.lastReminded] < self.remindPeriod * SECONDS_IN_A_DAY)
  408. {
  409. if (self.verboseLogging)
  410. {
  411. NSLog(@"iRate did not prompt for rating because the user last asked to be reminded less than %g days ago", self.remindPeriod);
  412. }
  413. return NO;
  414. }
  415. //lets prompt!
  416. return YES;
  417. }
  418. - (NSString *)valueForKey:(NSString *)key inJSON:(id)json
  419. {
  420. if ([json isKindOfClass:[NSString class]])
  421. {
  422. //use legacy parser
  423. NSRange keyRange = [json rangeOfString:[NSString stringWithFormat:@"\"%@\"", key]];
  424. if (keyRange.location != NSNotFound)
  425. {
  426. NSInteger start = keyRange.location + keyRange.length;
  427. NSRange valueStart = [json rangeOfString:@":" options:(NSStringCompareOptions)0 range:NSMakeRange(start, [(NSString *)json length] - start)];
  428. if (valueStart.location != NSNotFound)
  429. {
  430. start = valueStart.location + 1;
  431. NSRange valueEnd = [json rangeOfString:@"," options:(NSStringCompareOptions)0 range:NSMakeRange(start, [(NSString *)json length] - start)];
  432. if (valueEnd.location != NSNotFound)
  433. {
  434. NSString *value = [json substringWithRange:NSMakeRange(start, valueEnd.location - start)];
  435. value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  436. while ([value hasPrefix:@"\""] && ![value hasSuffix:@"\""])
  437. {
  438. if (valueEnd.location == NSNotFound)
  439. {
  440. break;
  441. }
  442. NSInteger newStart = valueEnd.location + 1;
  443. valueEnd = [json rangeOfString:@"," options:(NSStringCompareOptions)0 range:NSMakeRange(newStart, [(NSString *)json length] - newStart)];
  444. value = [json substringWithRange:NSMakeRange(start, valueEnd.location - start)];
  445. value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  446. }
  447. value = [value stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\""]];
  448. value = [value stringByReplacingOccurrencesOfString:@"\\\\" withString:@"\\"];
  449. value = [value stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
  450. value = [value stringByReplacingOccurrencesOfString:@"\\\"" withString:@"\""];
  451. value = [value stringByReplacingOccurrencesOfString:@"\\n" withString:@"\n"];
  452. value = [value stringByReplacingOccurrencesOfString:@"\\r" withString:@"\r"];
  453. value = [value stringByReplacingOccurrencesOfString:@"\\t" withString:@"\t"];
  454. value = [value stringByReplacingOccurrencesOfString:@"\\f" withString:@"\f"];
  455. value = [value stringByReplacingOccurrencesOfString:@"\\b" withString:@"\f"];
  456. while (YES)
  457. {
  458. NSRange unicode = [value rangeOfString:@"\\u"];
  459. if (unicode.location == NSNotFound || unicode.location + unicode.length == 0)
  460. {
  461. break;
  462. }
  463. uint32_t c = 0;
  464. NSString *hex = [value substringWithRange:NSMakeRange(unicode.location + 2, 4)];
  465. NSScanner *scanner = [NSScanner scannerWithString:hex];
  466. [scanner scanHexInt:&c];
  467. if (c <= 0xffff)
  468. {
  469. value = [value stringByReplacingCharactersInRange:NSMakeRange(unicode.location, 6) withString:[NSString stringWithFormat:@"%C", (unichar)c]];
  470. }
  471. else
  472. {
  473. //convert character to surrogate pair
  474. uint16_t x = (uint16_t)c;
  475. uint16_t u = (c >> 16) & ((1 << 5) - 1);
  476. uint16_t w = (uint16_t)u - 1;
  477. unichar high = 0xd800 | (w << 6) | x >> 10;
  478. unichar low = (uint16_t)(0xdc00 | (x & ((1 << 10) - 1)));
  479. value = [value stringByReplacingCharactersInRange:NSMakeRange(unicode.location, 6) withString:[NSString stringWithFormat:@"%C%C", high, low]];
  480. }
  481. }
  482. return value;
  483. }
  484. }
  485. }
  486. }
  487. else
  488. {
  489. return json[key];
  490. }
  491. return nil;
  492. }
  493. - (void)setAppStoreIDOnMainThread:(NSString *)appStoreIDString
  494. {
  495. _appStoreID = [appStoreIDString integerValue];
  496. [[NSUserDefaults standardUserDefaults] setInteger:_appStoreID forKey:iRateAppStoreIDKey];
  497. [[NSUserDefaults standardUserDefaults] synchronize];
  498. }
  499. - (void)connectionSucceeded
  500. {
  501. if (self.checkingForAppStoreID)
  502. {
  503. //no longer checking
  504. self.checkingForPrompt = NO;
  505. self.checkingForAppStoreID = NO;
  506. //open app store
  507. [self openRatingsPageInAppStore];
  508. }
  509. else if (self.checkingForPrompt)
  510. {
  511. //no longer checking
  512. self.checkingForPrompt = NO;
  513. //confirm with delegate
  514. if ([self.delegate respondsToSelector:@selector(iRateShouldPromptForRating)] && ![self.delegate iRateShouldPromptForRating])
  515. {
  516. if (self.verboseLogging)
  517. {
  518. NSLog(@"iRate did not display the rating prompt because the iRateShouldPromptForRating delegate method returned NO");
  519. }
  520. return;
  521. }
  522. //prompt user
  523. [self promptForRating: NO];
  524. }
  525. }
  526. - (void)connectionError:(NSError *)error
  527. {
  528. if (self.checkingForPrompt || self.checkingForAppStoreID)
  529. {
  530. //no longer checking
  531. self.checkingForPrompt = NO;
  532. self.checkingForAppStoreID = NO;
  533. //log the error
  534. if (error)
  535. {
  536. NSLog(@"iRate rating process failed because: %@", [error localizedDescription]);
  537. }
  538. else
  539. {
  540. NSLog(@"iRate rating process failed because an unknown error occurred");
  541. }
  542. //could not connect
  543. if ([self.delegate respondsToSelector:@selector(iRateCouldNotConnectToAppStore:)])
  544. {
  545. [self.delegate iRateCouldNotConnectToAppStore:error];
  546. }
  547. [[NSNotificationCenter defaultCenter] postNotificationName:iRateCouldNotConnectToAppStore
  548. object:error];
  549. }
  550. }
  551. - (void)checkForConnectivityInBackground
  552. {
  553. #if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0) \
  554. || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_10)
  555. #define IRATE_BACKGROUND_QUEUE QOS_CLASS_BACKGROUND
  556. #else
  557. #define IRATE_BACKGROUND_QUEUE DISPATCH_QUEUE_PRIORITY_BACKGROUND
  558. #endif
  559. dispatch_async(dispatch_get_global_queue(IRATE_BACKGROUND_QUEUE, 0), ^{
  560. [self checkForConnectivity];
  561. });
  562. }
  563. - (void)checkForConnectivity
  564. {
  565. @autoreleasepool
  566. {
  567. //first check iTunes
  568. NSString *iTunesServiceURL = [NSString stringWithFormat:iRateAppLookupURLFormat, self.appStoreCountry];
  569. if (_appStoreID) //important that we check ivar and not getter in case it has changed
  570. {
  571. iTunesServiceURL = [iTunesServiceURL stringByAppendingFormat:@"?id=%@", @(_appStoreID)];
  572. }
  573. else
  574. {
  575. iTunesServiceURL = [iTunesServiceURL stringByAppendingFormat:@"?bundleId=%@", self.applicationBundleID];
  576. }
  577. if (self.verboseLogging)
  578. {
  579. NSLog(@"iRate is checking %@ to retrieve the App Store details...", iTunesServiceURL);
  580. }
  581. NSError *error = nil;
  582. NSURLResponse *response = nil;
  583. NSURL *url = [NSURL URLWithString:iTunesServiceURL];
  584. NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:REQUEST_TIMEOUT];
  585. #pragma clang diagnostic push
  586. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  587. NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
  588. #pragma clang diagnostic pop
  589. NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
  590. if (data && statusCode == 200)
  591. {
  592. //in case error is garbage...
  593. error = nil;
  594. id json = nil;
  595. if ([NSJSONSerialization class])
  596. {
  597. json = [[NSJSONSerialization JSONObjectWithData:data options:(NSJSONReadingOptions)0 error:&error][@"results"] lastObject];
  598. }
  599. else
  600. {
  601. //convert to string
  602. json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  603. }
  604. if (!error)
  605. {
  606. //check bundle ID matches
  607. NSString *bundleID = [self valueForKey:@"bundleId" inJSON:json];
  608. if (bundleID)
  609. {
  610. if ([bundleID.lowercaseString isEqualToString:self.applicationBundleID.lowercaseString])
  611. {
  612. //get genre
  613. if (self.appStoreGenreID == 0)
  614. {
  615. self.appStoreGenreID = [[self valueForKey:@"primaryGenreId" inJSON:json] integerValue];
  616. }
  617. //get app id
  618. if (!_appStoreID)
  619. {
  620. NSString *appStoreIDString = [self valueForKey:@"trackId" inJSON:json];
  621. dispatch_sync(dispatch_get_main_queue(), ^{
  622. [self setAppStoreIDOnMainThread:appStoreIDString];
  623. });
  624. if (self.verboseLogging)
  625. {
  626. NSLog(@"iRate found the app on iTunes. The App Store ID is %@", appStoreIDString);
  627. }
  628. }
  629. //check version
  630. if (self.onlyPromptIfLatestVersion && !self.previewMode)
  631. {
  632. NSString *latestVersion = [self valueForKey:@"version" inJSON:json];
  633. if ([latestVersion compare:self.applicationVersion options:NSNumericSearch] == NSOrderedDescending)
  634. {
  635. if (self.verboseLogging)
  636. {
  637. NSLog(@"iRate found that the installed application version (%@) is not the latest version on the App Store, which is %@", self.applicationVersion, latestVersion);
  638. }
  639. error = [NSError errorWithDomain:iRateErrorDomain code:iRateErrorApplicationIsNotLatestVersion userInfo:@{NSLocalizedDescriptionKey: @"Installed app is not the latest version available"}];
  640. }
  641. }
  642. }
  643. else
  644. {
  645. if (self.verboseLogging)
  646. {
  647. 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));
  648. }
  649. error = [NSError errorWithDomain:iRateErrorDomain code:iRateErrorBundleIdDoesNotMatchAppStore userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Application bundle ID does not match expected value of %@", bundleID]}];
  650. }
  651. }
  652. else if (_appStoreID || !self.ratingsURL)
  653. {
  654. if (self.verboseLogging)
  655. {
  656. 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");
  657. }
  658. if (!self.previewMode)
  659. {
  660. error = [NSError errorWithDomain:iRateErrorDomain
  661. code:iRateErrorApplicationNotFoundOnAppStore
  662. userInfo:@{NSLocalizedDescriptionKey: @"The application could not be found on the App Store."}];
  663. }
  664. }
  665. else if (!_appStoreID && self.verboseLogging)
  666. {
  667. 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");
  668. }
  669. }
  670. }
  671. else if (statusCode >= 400)
  672. {
  673. //http error
  674. NSString *message = [NSString stringWithFormat:@"The server returned a %@ error", @(statusCode)];
  675. error = [NSError errorWithDomain:@"HTTPResponseErrorDomain" code:statusCode userInfo:@{NSLocalizedDescriptionKey: message}];
  676. }
  677. dispatch_async(dispatch_get_main_queue(), ^{
  678. //handle errors (ignoring sandbox issues)
  679. if (error && !(error.code == EPERM && [error.domain isEqualToString:NSPOSIXErrorDomain] && self.appStoreID))
  680. {
  681. [self connectionError:error];
  682. }
  683. else if (self.appStoreID || self.previewMode)
  684. {
  685. //show prompt
  686. [self connectionSucceeded];
  687. }
  688. });
  689. }
  690. }
  691. - (void)promptIfNetworkAvailable
  692. {
  693. if (!self.checkingForPrompt && !self.checkingForAppStoreID)
  694. {
  695. self.checkingForPrompt = YES;
  696. [self checkForConnectivityInBackground];
  697. }
  698. }
  699. - (void)promptIfAllCriteriaMet
  700. {
  701. if ([self shouldPromptForRating])
  702. {
  703. [self promptIfNetworkAvailable];
  704. }
  705. }
  706. - (BOOL)showRemindButton
  707. {
  708. return [self.remindButtonLabel length];
  709. }
  710. - (BOOL)showCancelButton
  711. {
  712. return [self.cancelButtonLabel length];
  713. }
  714. - (void)promptForRating
  715. {
  716. [self promptForRating: YES];
  717. }
  718. - (void)promptForRating:(BOOL)manual
  719. {
  720. if (!self.visibleAlert)
  721. {
  722. NSString *message = self.ratedAnyVersion? self.updateMessage: self.message;
  723. #if TARGET_OS_IPHONE
  724. #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_10_2
  725. if (!manual && [SKStoreReviewController class])
  726. {
  727. [self remindLater];
  728. [SKStoreReviewController requestReview];
  729. }
  730. else
  731. #endif
  732. {
  733. UIViewController *topController = [UIApplication sharedApplication].delegate.window.rootViewController;
  734. while (topController.presentedViewController)
  735. {
  736. topController = topController.presentedViewController;
  737. }
  738. if ([UIAlertController class] && topController && self.useUIAlertControllerIfAvailable)
  739. {
  740. UIAlertController *alert = [UIAlertController alertControllerWithTitle:self.messageTitle message:message preferredStyle:UIAlertControllerStyleAlert];
  741. //rate action
  742. [alert addAction:[UIAlertAction actionWithTitle:self.rateButtonLabel style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) {
  743. [self didDismissAlert:alert withButtonAtIndex:0];
  744. }]];
  745. //cancel action
  746. if ([self showCancelButton])
  747. {
  748. [alert addAction:[UIAlertAction actionWithTitle:self.cancelButtonLabel style:UIAlertActionStyleCancel handler:^(__unused UIAlertAction *action) {
  749. [self didDismissAlert:alert withButtonAtIndex:1];
  750. }]];
  751. }
  752. //remind action
  753. if ([self showRemindButton])
  754. {
  755. [alert addAction:[UIAlertAction actionWithTitle:self.remindButtonLabel style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) {
  756. [self didDismissAlert:alert withButtonAtIndex:[self showCancelButton]? 2: 1];
  757. }]];
  758. }
  759. self.visibleAlert = alert;
  760. //get current view controller and present alert
  761. [topController presentViewController:alert animated:YES completion:NULL];
  762. }
  763. else
  764. {
  765. #pragma clang diagnostic push
  766. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  767. UIAlertView *alert = [[UIAlertView alloc] initWithTitle:self.messageTitle
  768. message:message
  769. delegate:(id<UIAlertViewDelegate>)self
  770. cancelButtonTitle:nil
  771. otherButtonTitles:self.rateButtonLabel, nil];
  772. #pragma clang diagnostic pop
  773. if ([self showCancelButton])
  774. {
  775. [alert addButtonWithTitle:self.cancelButtonLabel];
  776. alert.cancelButtonIndex = 1;
  777. }
  778. if ([self showRemindButton])
  779. {
  780. [alert addButtonWithTitle:self.remindButtonLabel];
  781. }
  782. self.visibleAlert = alert;
  783. [self.visibleAlert show];
  784. }
  785. }
  786. #else
  787. //only show when main window is available
  788. if (self.onlyPromptIfMainWindowIsAvailable && ![[NSApplication sharedApplication] mainWindow])
  789. {
  790. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  791. [self promptForRating: NO];
  792. });
  793. return;
  794. }
  795. NSAlert *alert = [[NSAlert alloc] init];
  796. alert.messageText = self.messageTitle;
  797. alert.informativeText = message;
  798. [alert addButtonWithTitle:self.rateButtonLabel];
  799. if ([self showCancelButton])
  800. {
  801. [alert addButtonWithTitle:self.cancelButtonLabel];
  802. }
  803. if ([self showRemindButton])
  804. {
  805. [alert addButtonWithTitle:self.remindButtonLabel];
  806. }
  807. self.visibleAlert = alert;
  808. #if __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9
  809. if (![alert respondsToSelector:@selector(beginSheetModalForWindow:completionHandler:)])
  810. {
  811. [alert beginSheetModalForWindow:(__nonnull id)[NSApplication sharedApplication].mainWindow
  812. modalDelegate:self
  813. didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
  814. contextInfo:nil];
  815. }
  816. else
  817. #endif
  818. {
  819. [alert beginSheetModalForWindow:(__nonnull id)[NSApplication sharedApplication].mainWindow completionHandler:^(NSModalResponse returnCode) {
  820. [self didDismissAlert:alert withButtonAtIndex:returnCode - NSAlertFirstButtonReturn];
  821. }];
  822. }
  823. #endif
  824. //inform about prompt
  825. if ([self.delegate respondsToSelector:@selector(iRateDidPromptForRating)])
  826. {
  827. [self.delegate iRateDidPromptForRating];
  828. }
  829. [[NSNotificationCenter defaultCenter] postNotificationName:iRateDidPromptForRating
  830. object:nil];
  831. }
  832. }
  833. - (void)applicationLaunched
  834. {
  835. //check if this is a new version
  836. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  837. NSString *lastUsedVersion = [defaults objectForKey:iRateLastVersionUsedKey];
  838. if (!self.firstUsed || ![lastUsedVersion isEqualToString:self.applicationVersion])
  839. {
  840. [defaults setObject:self.applicationVersion forKey:iRateLastVersionUsedKey];
  841. if (!self.firstUsed || [self ratedAnyVersion])
  842. {
  843. //reset defaults
  844. [defaults setObject:[NSDate date] forKey:iRateFirstUsedKey];
  845. [defaults setInteger:0 forKey:iRateUseCountKey];
  846. [defaults setInteger:0 forKey:iRateEventCountKey];
  847. [defaults setObject:nil forKey:iRateLastRemindedKey];
  848. [defaults synchronize];
  849. }
  850. else if ([[NSDate date] timeIntervalSinceDate:self.firstUsed] > (self.daysUntilPrompt - 1) * SECONDS_IN_A_DAY)
  851. {
  852. //if was previously installed, but we haven't yet prompted for a rating
  853. //don't reset, but make sure it won't rate for a day at least
  854. self.firstUsed = [[NSDate date] dateByAddingTimeInterval:(self.daysUntilPrompt - 1) * -SECONDS_IN_A_DAY];
  855. }
  856. //inform about app update
  857. if ([self.delegate respondsToSelector:@selector(iRateDidDetectAppUpdate)])
  858. {
  859. [self.delegate iRateDidDetectAppUpdate];
  860. }
  861. [[NSNotificationCenter defaultCenter] postNotificationName:iRateDidDetectAppUpdate
  862. object:nil];
  863. }
  864. [self incrementUseCount];
  865. [self checkForConnectivityInBackground];
  866. if (self.promptAtLaunch)
  867. {
  868. [self promptIfAllCriteriaMet];
  869. }
  870. }
  871. - (void)didDismissAlert:(__unused id)alertView withButtonAtIndex:(NSInteger)buttonIndex
  872. {
  873. dispatch_async(dispatch_get_main_queue(), ^{
  874. //get button indices
  875. NSInteger rateButtonIndex = 0;
  876. NSInteger cancelButtonIndex = [self showCancelButton]? 1: 0;
  877. NSInteger remindButtonIndex = [self showRemindButton]? cancelButtonIndex + 1: 0;
  878. if (buttonIndex == rateButtonIndex)
  879. {
  880. [self rate];
  881. }
  882. else if (buttonIndex == cancelButtonIndex)
  883. {
  884. [self declineThisVersion];
  885. }
  886. else if (buttonIndex == remindButtonIndex)
  887. {
  888. [self remindLater];
  889. }
  890. //release alert
  891. self.visibleAlert = nil;
  892. });
  893. }
  894. #if TARGET_OS_IPHONE
  895. - (void)applicationWillEnterForeground
  896. {
  897. if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground)
  898. {
  899. [self incrementUseCount];
  900. [self checkForConnectivityInBackground];
  901. if (self.promptAtLaunch)
  902. {
  903. [self promptIfAllCriteriaMet];
  904. }
  905. }
  906. }
  907. - (void)openRatingsPageInAppStore
  908. {
  909. if (!_ratingsURL && !self.appStoreID)
  910. {
  911. self.checkingForAppStoreID = YES;
  912. if (!self.checkingForPrompt)
  913. {
  914. [self checkForConnectivityInBackground];
  915. }
  916. return;
  917. }
  918. NSString *cantOpenMessage = nil;
  919. #if TARGET_IPHONE_SIMULATOR
  920. if ([[self.ratingsURL scheme] isEqualToString:iRateiOSAppStoreURLScheme])
  921. {
  922. cantOpenMessage = @"iRate could not open the ratings page because the App Store is not available on the iOS simulator";
  923. }
  924. #endif
  925. void (^handler)(NSString *errorMessage) = ^(NSString *errorMessage)
  926. {
  927. if (errorMessage)
  928. {
  929. NSLog(@"%@", errorMessage);
  930. NSError *error = [NSError errorWithDomain:iRateErrorDomain code:iRateErrorCouldNotOpenRatingPageURL userInfo:@{NSLocalizedDescriptionKey: errorMessage}];
  931. if ([self.delegate respondsToSelector:@selector(iRateCouldNotConnectToAppStore:)])
  932. {
  933. [self.delegate iRateCouldNotConnectToAppStore:error];
  934. }
  935. [[NSNotificationCenter defaultCenter] postNotificationName:iRateCouldNotConnectToAppStore
  936. object:error];
  937. }
  938. else
  939. {
  940. if ([self.delegate respondsToSelector:@selector(iRateDidOpenAppStore)])
  941. {
  942. [self.delegate iRateDidOpenAppStore];
  943. }
  944. [[NSNotificationCenter defaultCenter] postNotificationName:iRateDidOpenAppStore
  945. object:nil];
  946. }
  947. };
  948. if (cantOpenMessage)
  949. {
  950. handler(cantOpenMessage);
  951. }
  952. else
  953. {
  954. if (self.verboseLogging)
  955. {
  956. NSLog(@"iRate will open the App Store ratings page using the following URL: %@", self.ratingsURL);
  957. }
  958. #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_10_0
  959. [[UIApplication sharedApplication] openURL:self.ratingsURL options:@{} completionHandler:^(BOOL success){
  960. if (success)
  961. {
  962. handler(nil);
  963. }
  964. else
  965. {
  966. handler([NSString stringWithFormat:@"iRate was unable to open the specified ratings URL: %@", self.ratingsURL]);
  967. }
  968. }];
  969. #else
  970. if ([[UIApplication sharedApplication] openURL:self.ratingsURL])
  971. {
  972. handler(nil);
  973. }
  974. else
  975. {
  976. handler([NSString stringWithFormat:@"iRate was unable to open the specified ratings URL: %@", self.ratingsURL]);
  977. }
  978. #endif
  979. }
  980. }
  981. #pragma clang diagnostic push
  982. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  983. - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
  984. {
  985. [self didDismissAlert:alertView withButtonAtIndex:buttonIndex];
  986. }
  987. #pragma clang diagnostic pop
  988. #else
  989. - (void)openAppPageWhenAppStoreLaunched
  990. {
  991. //check if app store is running
  992. for (NSRunningApplication *app in [[NSWorkspace sharedWorkspace] runningApplications])
  993. {
  994. if ([app.bundleIdentifier isEqualToString:iRateMacAppStoreBundleID])
  995. {
  996. //open app page
  997. [[NSWorkspace sharedWorkspace] performSelector:@selector(openURL:) withObject:self.ratingsURL afterDelay:MAC_APP_STORE_REFRESH_DELAY];
  998. return;
  999. }
  1000. }
  1001. //try again
  1002. [self performSelector:@selector(openAppPageWhenAppStoreLaunched) withObject:nil afterDelay:0.0];
  1003. }
  1004. - (void)openRatingsPageInAppStore
  1005. {
  1006. if (!_ratingsURL && !self.appStoreID)
  1007. {
  1008. self.checkingForAppStoreID = YES;
  1009. if (!self.checkingForPrompt)
  1010. {
  1011. [self checkForConnectivityInBackground];
  1012. }
  1013. return;
  1014. }
  1015. if (self.verboseLogging)
  1016. {
  1017. NSLog(@"iRate will open the App Store ratings page using the following URL: %@", self.ratingsURL);
  1018. }
  1019. [[NSWorkspace sharedWorkspace] openURL:self.ratingsURL];
  1020. [self openAppPageWhenAppStoreLaunched];
  1021. if ([self.delegate respondsToSelector:@selector(iRateDidOpenAppStore)])
  1022. {
  1023. [self.delegate iRateDidOpenAppStore];
  1024. }
  1025. }
  1026. - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(__unused void *)contextInfo
  1027. {
  1028. [self didDismissAlert:alert withButtonAtIndex:returnCode - NSAlertFirstButtonReturn];
  1029. }
  1030. #endif
  1031. - (void)logEvent:(BOOL)deferPrompt
  1032. {
  1033. [self incrementEventCount];
  1034. if (!deferPrompt)
  1035. {
  1036. [self promptIfAllCriteriaMet];
  1037. }
  1038. }
  1039. #pragma mark - User's actions
  1040. - (void)declineThisVersion
  1041. {
  1042. //ignore this version
  1043. self.declinedThisVersion = YES;
  1044. //log event
  1045. if ([self.delegate respondsToSelector:@selector(iRateUserDidDeclineToRateApp)])
  1046. {
  1047. [self.delegate iRateUserDidDeclineToRateApp];
  1048. }
  1049. [[NSNotificationCenter defaultCenter] postNotificationName:iRateUserDidDeclineToRateApp
  1050. object:nil];
  1051. }
  1052. - (void)remindLater
  1053. {
  1054. //remind later
  1055. self.lastReminded = [NSDate date];
  1056. //log event
  1057. if ([self.delegate respondsToSelector:@selector(iRateUserDidRequestReminderToRateApp)])
  1058. {
  1059. [self.delegate iRateUserDidRequestReminderToRateApp];
  1060. }
  1061. [[NSNotificationCenter defaultCenter] postNotificationName:iRateUserDidRequestReminderToRateApp
  1062. object:nil];
  1063. }
  1064. - (void)rate
  1065. {
  1066. //mark as rated
  1067. self.ratedThisVersion = YES;
  1068. //log event
  1069. if ([self.delegate respondsToSelector:@selector(iRateUserDidAttemptToRateApp)])
  1070. {
  1071. [self.delegate iRateUserDidAttemptToRateApp];
  1072. }
  1073. [[NSNotificationCenter defaultCenter] postNotificationName:iRateUserDidAttemptToRateApp
  1074. object:nil];
  1075. // if the delegate has not implemented the method, or if it returns YES
  1076. if (![self.delegate respondsToSelector:@selector(iRateShouldOpenAppStore)] || [self.delegate iRateShouldOpenAppStore])
  1077. {
  1078. //launch mac app store
  1079. [self openRatingsPageInAppStore];
  1080. }
  1081. }
  1082. @end