|
1 /* |
|
2 This file is part of Appirater. |
|
3 |
|
4 Copyright (c) 2012, Arash Payan |
|
5 All rights reserved. |
|
6 |
|
7 Permission is hereby granted, free of charge, to any person |
|
8 obtaining a copy of this software and associated documentation |
|
9 files (the "Software"), to deal in the Software without |
|
10 restriction, including without limitation the rights to use, |
|
11 copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
12 copies of the Software, and to permit persons to whom the |
|
13 Software is furnished to do so, subject to the following |
|
14 conditions: |
|
15 |
|
16 The above copyright notice and this permission notice shall be |
|
17 included in all copies or substantial portions of the Software. |
|
18 |
|
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|
20 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES |
|
21 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|
22 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
|
23 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
|
24 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|
25 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR |
|
26 OTHER DEALINGS IN THE SOFTWARE. |
|
27 */ |
|
28 /* |
|
29 * Appirater.m |
|
30 * appirater |
|
31 * |
|
32 * Created by Arash Payan on 9/5/09. |
|
33 * http://arashpayan.com |
|
34 * Copyright 2012 Arash Payan. All rights reserved. |
|
35 */ |
|
36 |
|
37 #import "Appirater.h" |
|
38 #import <SystemConfiguration/SCNetworkReachability.h> |
|
39 #include <netinet/in.h> |
|
40 |
|
41 #if ! __has_feature(objc_arc) |
|
42 #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). |
|
43 #endif |
|
44 |
|
45 NSString *const kAppiraterFirstUseDate = @"kAppiraterFirstUseDate"; |
|
46 NSString *const kAppiraterUseCount = @"kAppiraterUseCount"; |
|
47 NSString *const kAppiraterSignificantEventCount = @"kAppiraterSignificantEventCount"; |
|
48 NSString *const kAppiraterCurrentVersion = @"kAppiraterCurrentVersion"; |
|
49 NSString *const kAppiraterRatedCurrentVersion = @"kAppiraterRatedCurrentVersion"; |
|
50 NSString *const kAppiraterDeclinedToRate = @"kAppiraterDeclinedToRate"; |
|
51 NSString *const kAppiraterReminderRequestDate = @"kAppiraterReminderRequestDate"; |
|
52 |
|
53 NSString *templateReviewURL = @"itms-apps://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?type=Purple+Software&id=APP_ID"; |
|
54 NSString *templateReviewURLiOS7 = @"itms-apps://itunes.apple.com/app/idAPP_ID"; |
|
55 NSString *templateReviewURLiOS8 = @"itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=APP_ID&onlyLatestVersion=true&pageNumber=0&sortOrdering=1&type=Purple+Software"; |
|
56 |
|
57 static NSString *_appId; |
|
58 static double _daysUntilPrompt = 30; |
|
59 static NSInteger _usesUntilPrompt = 20; |
|
60 static NSInteger _significantEventsUntilPrompt = -1; |
|
61 static double _timeBeforeReminding = 1; |
|
62 static BOOL _debug = NO; |
|
63 #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_5_0 |
|
64 static id<AppiraterDelegate> _delegate; |
|
65 #else |
|
66 __weak static id<AppiraterDelegate> _delegate; |
|
67 #endif |
|
68 static BOOL _usesAnimation = TRUE; |
|
69 static UIStatusBarStyle _statusBarStyle; |
|
70 static BOOL _modalOpen = false; |
|
71 static BOOL _alwaysUseMainBundle = NO; |
|
72 |
|
73 @interface Appirater () |
|
74 @property (nonatomic, copy) NSString *alertTitle; |
|
75 @property (nonatomic, copy) NSString *alertMessage; |
|
76 @property (nonatomic, copy) NSString *alertCancelTitle; |
|
77 @property (nonatomic, copy) NSString *alertRateTitle; |
|
78 @property (nonatomic, copy) NSString *alertRateLaterTitle; |
|
79 - (BOOL)connectedToNetwork; |
|
80 + (Appirater*)sharedInstance; |
|
81 - (void)showPromptWithChecks:(BOOL)withChecks |
|
82 displayRateLaterButton:(BOOL)displayRateLaterButton; |
|
83 - (void)showRatingAlert:(BOOL)displayRateLaterButton; |
|
84 - (void)showRatingAlert; |
|
85 - (BOOL)ratingAlertIsAppropriate; |
|
86 - (BOOL)ratingConditionsHaveBeenMet; |
|
87 - (void)incrementUseCount; |
|
88 - (void)hideRatingAlert; |
|
89 @end |
|
90 |
|
91 @implementation Appirater |
|
92 |
|
93 @synthesize ratingAlert; |
|
94 |
|
95 + (void) setAppId:(NSString *)appId { |
|
96 _appId = appId; |
|
97 } |
|
98 |
|
99 + (void) setDaysUntilPrompt:(double)value { |
|
100 _daysUntilPrompt = value; |
|
101 } |
|
102 |
|
103 + (void) setUsesUntilPrompt:(NSInteger)value { |
|
104 _usesUntilPrompt = value; |
|
105 } |
|
106 |
|
107 + (void) setSignificantEventsUntilPrompt:(NSInteger)value { |
|
108 _significantEventsUntilPrompt = value; |
|
109 } |
|
110 |
|
111 + (void) setTimeBeforeReminding:(double)value { |
|
112 _timeBeforeReminding = value; |
|
113 } |
|
114 |
|
115 + (void) setCustomAlertTitle:(NSString *)title |
|
116 { |
|
117 [self sharedInstance].alertTitle = title; |
|
118 } |
|
119 |
|
120 + (void) setCustomAlertMessage:(NSString *)message |
|
121 { |
|
122 [self sharedInstance].alertMessage = message; |
|
123 } |
|
124 |
|
125 + (void) setCustomAlertCancelButtonTitle:(NSString *)cancelTitle |
|
126 { |
|
127 [self sharedInstance].alertCancelTitle = cancelTitle; |
|
128 } |
|
129 |
|
130 + (void) setCustomAlertRateButtonTitle:(NSString *)rateTitle |
|
131 { |
|
132 [self sharedInstance].alertRateTitle = rateTitle; |
|
133 } |
|
134 |
|
135 + (void) setCustomAlertRateLaterButtonTitle:(NSString *)rateLaterTitle |
|
136 { |
|
137 [self sharedInstance].alertRateLaterTitle = rateLaterTitle; |
|
138 } |
|
139 |
|
140 + (void) setDebug:(BOOL)debug { |
|
141 _debug = debug; |
|
142 } |
|
143 + (void)setDelegate:(id<AppiraterDelegate>)delegate{ |
|
144 _delegate = delegate; |
|
145 } |
|
146 + (void)setUsesAnimation:(BOOL)animation { |
|
147 _usesAnimation = animation; |
|
148 } |
|
149 + (void)setOpenInAppStore:(BOOL)openInAppStore { |
|
150 [Appirater sharedInstance].openInAppStore = openInAppStore; |
|
151 } |
|
152 + (void)setStatusBarStyle:(UIStatusBarStyle)style { |
|
153 _statusBarStyle = style; |
|
154 } |
|
155 + (void)setModalOpen:(BOOL)open { |
|
156 _modalOpen = open; |
|
157 } |
|
158 + (void)setAlwaysUseMainBundle:(BOOL)alwaysUseMainBundle { |
|
159 _alwaysUseMainBundle = alwaysUseMainBundle; |
|
160 } |
|
161 |
|
162 + (NSBundle *)bundle |
|
163 { |
|
164 NSBundle *bundle; |
|
165 |
|
166 if (_alwaysUseMainBundle) { |
|
167 bundle = [NSBundle mainBundle]; |
|
168 } else { |
|
169 NSURL *appiraterBundleURL = [[NSBundle mainBundle] URLForResource:@"Appirater" withExtension:@"bundle"]; |
|
170 |
|
171 if (appiraterBundleURL) { |
|
172 // Appirater.bundle will likely only exist when used via CocoaPods |
|
173 bundle = [NSBundle bundleWithURL:appiraterBundleURL]; |
|
174 } else { |
|
175 bundle = [NSBundle mainBundle]; |
|
176 } |
|
177 } |
|
178 |
|
179 return bundle; |
|
180 } |
|
181 |
|
182 - (NSString *)alertTitle |
|
183 { |
|
184 return _alertTitle ? _alertTitle : APPIRATER_MESSAGE_TITLE; |
|
185 } |
|
186 |
|
187 - (NSString *)alertMessage |
|
188 { |
|
189 return _alertMessage ? _alertMessage : APPIRATER_MESSAGE; |
|
190 } |
|
191 |
|
192 - (NSString *)alertCancelTitle |
|
193 { |
|
194 return _alertCancelTitle ? _alertCancelTitle : APPIRATER_CANCEL_BUTTON; |
|
195 } |
|
196 |
|
197 - (NSString *)alertRateTitle |
|
198 { |
|
199 return _alertRateTitle ? _alertRateTitle : APPIRATER_RATE_BUTTON; |
|
200 } |
|
201 |
|
202 - (NSString *)alertRateLaterTitle |
|
203 { |
|
204 return _alertRateLaterTitle ? _alertRateLaterTitle : APPIRATER_RATE_LATER; |
|
205 } |
|
206 |
|
207 - (void)dealloc { |
|
208 [[NSNotificationCenter defaultCenter] removeObserver:self]; |
|
209 } |
|
210 |
|
211 - (id)init { |
|
212 self = [super init]; |
|
213 if (self) { |
|
214 if ([[UIDevice currentDevice].systemVersion floatValue] >= 7.0) { |
|
215 self.openInAppStore = YES; |
|
216 } else { |
|
217 self.openInAppStore = NO; |
|
218 } |
|
219 } |
|
220 |
|
221 return self; |
|
222 } |
|
223 |
|
224 - (BOOL)connectedToNetwork { |
|
225 // Create zero addy |
|
226 struct sockaddr_in zeroAddress; |
|
227 bzero(&zeroAddress, sizeof(zeroAddress)); |
|
228 zeroAddress.sin_len = sizeof(zeroAddress); |
|
229 zeroAddress.sin_family = AF_INET; |
|
230 |
|
231 // Recover reachability flags |
|
232 SCNetworkReachabilityRef defaultRouteReachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress); |
|
233 SCNetworkReachabilityFlags flags; |
|
234 |
|
235 Boolean didRetrieveFlags = SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags); |
|
236 CFRelease(defaultRouteReachability); |
|
237 |
|
238 if (!didRetrieveFlags) |
|
239 { |
|
240 NSLog(@"Error. Could not recover network reachability flags"); |
|
241 return NO; |
|
242 } |
|
243 |
|
244 BOOL isReachable = flags & kSCNetworkFlagsReachable; |
|
245 BOOL needsConnection = flags & kSCNetworkFlagsConnectionRequired; |
|
246 BOOL nonWiFi = flags & kSCNetworkReachabilityFlagsTransientConnection; |
|
247 |
|
248 NSURL *testURL = [NSURL URLWithString:@"http://www.apple.com/"]; |
|
249 NSURLRequest *testRequest = [NSURLRequest requestWithURL:testURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:20.0]; |
|
250 NSURLConnection *testConnection = [[NSURLConnection alloc] initWithRequest:testRequest delegate:self]; |
|
251 |
|
252 return ((isReachable && !needsConnection) || nonWiFi) ? (testConnection ? YES : NO) : NO; |
|
253 } |
|
254 |
|
255 + (Appirater*)sharedInstance { |
|
256 static Appirater *appirater = nil; |
|
257 if (appirater == nil) |
|
258 { |
|
259 static dispatch_once_t onceToken; |
|
260 dispatch_once(&onceToken, ^{ |
|
261 appirater = [[Appirater alloc] init]; |
|
262 appirater.delegate = _delegate; |
|
263 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActive) name: |
|
264 UIApplicationWillResignActiveNotification object:nil]; |
|
265 }); |
|
266 } |
|
267 |
|
268 return appirater; |
|
269 } |
|
270 |
|
271 - (void)showRatingAlert:(BOOL)displayRateLaterButton { |
|
272 UIAlertView *alertView = nil; |
|
273 id <AppiraterDelegate> delegate = _delegate; |
|
274 |
|
275 if(delegate && [delegate respondsToSelector:@selector(appiraterShouldDisplayAlert:)] && ![delegate appiraterShouldDisplayAlert:self]) { |
|
276 return; |
|
277 } |
|
278 |
|
279 if (displayRateLaterButton) { |
|
280 alertView = [[UIAlertView alloc] initWithTitle:self.alertTitle |
|
281 message:self.alertMessage |
|
282 delegate:self |
|
283 cancelButtonTitle:self.alertCancelTitle |
|
284 otherButtonTitles:self.alertRateTitle, self.alertRateLaterTitle, nil]; |
|
285 } else { |
|
286 alertView = [[UIAlertView alloc] initWithTitle:self.alertTitle |
|
287 message:self.alertMessage |
|
288 delegate:self |
|
289 cancelButtonTitle:self.alertCancelTitle |
|
290 otherButtonTitles:self.alertRateTitle, nil]; |
|
291 } |
|
292 |
|
293 self.ratingAlert = alertView; |
|
294 [alertView show]; |
|
295 |
|
296 if (delegate && [delegate respondsToSelector:@selector(appiraterDidDisplayAlert:)]) { |
|
297 [delegate appiraterDidDisplayAlert:self]; |
|
298 } |
|
299 } |
|
300 |
|
301 - (void)showRatingAlert |
|
302 { |
|
303 [self showRatingAlert:true]; |
|
304 } |
|
305 |
|
306 // is this an ok time to show the alert? (regardless of whether the rating conditions have been met) |
|
307 // |
|
308 // things checked here: |
|
309 // * connectivity with network |
|
310 // * whether user has rated before |
|
311 // * whether user has declined to rate |
|
312 // * whether rating alert is currently showing visibly |
|
313 // things NOT checked here: |
|
314 // * time since first launch |
|
315 // * number of uses of app |
|
316 // * number of significant events |
|
317 // * time since last reminder |
|
318 - (BOOL)ratingAlertIsAppropriate { |
|
319 return ([self connectedToNetwork] |
|
320 && ![self userHasDeclinedToRate] |
|
321 && !self.ratingAlert.visible |
|
322 && ![self userHasRatedCurrentVersion]); |
|
323 } |
|
324 |
|
325 // have the rating conditions been met/earned? (regardless of whether this would be a moment when it's appropriate to show a new rating alert) |
|
326 // |
|
327 // things checked here: |
|
328 // * time since first launch |
|
329 // * number of uses of app |
|
330 // * number of significant events |
|
331 // * time since last reminder |
|
332 // things NOT checked here: |
|
333 // * connectivity with network |
|
334 // * whether user has rated before |
|
335 // * whether user has declined to rate |
|
336 // * whether rating alert is currently showing visibly |
|
337 - (BOOL)ratingConditionsHaveBeenMet { |
|
338 if (_debug) |
|
339 return YES; |
|
340 |
|
341 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; |
|
342 |
|
343 NSDate *dateOfFirstLaunch = [NSDate dateWithTimeIntervalSince1970:[userDefaults doubleForKey:kAppiraterFirstUseDate]]; |
|
344 NSTimeInterval timeSinceFirstLaunch = [[NSDate date] timeIntervalSinceDate:dateOfFirstLaunch]; |
|
345 NSTimeInterval timeUntilRate = 60 * 60 * 24 * _daysUntilPrompt; |
|
346 if (timeSinceFirstLaunch < timeUntilRate) |
|
347 return NO; |
|
348 |
|
349 // check if the app has been used enough |
|
350 NSInteger useCount = [userDefaults integerForKey:kAppiraterUseCount]; |
|
351 if (useCount < _usesUntilPrompt) |
|
352 return NO; |
|
353 |
|
354 // check if the user has done enough significant events |
|
355 NSInteger sigEventCount = [userDefaults integerForKey:kAppiraterSignificantEventCount]; |
|
356 if (sigEventCount < _significantEventsUntilPrompt) |
|
357 return NO; |
|
358 |
|
359 // if the user wanted to be reminded later, has enough time passed? |
|
360 NSDate *reminderRequestDate = [NSDate dateWithTimeIntervalSince1970:[userDefaults doubleForKey:kAppiraterReminderRequestDate]]; |
|
361 NSTimeInterval timeSinceReminderRequest = [[NSDate date] timeIntervalSinceDate:reminderRequestDate]; |
|
362 NSTimeInterval timeUntilReminder = 60 * 60 * 24 * _timeBeforeReminding; |
|
363 if (timeSinceReminderRequest < timeUntilReminder) |
|
364 return NO; |
|
365 |
|
366 return YES; |
|
367 } |
|
368 |
|
369 - (void)incrementUseCount { |
|
370 // get the app's version |
|
371 NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleVersionKey]; |
|
372 |
|
373 // get the version number that we've been tracking |
|
374 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; |
|
375 NSString *trackingVersion = [userDefaults stringForKey:kAppiraterCurrentVersion]; |
|
376 if (trackingVersion == nil) |
|
377 { |
|
378 trackingVersion = version; |
|
379 [userDefaults setObject:version forKey:kAppiraterCurrentVersion]; |
|
380 } |
|
381 |
|
382 if (_debug) |
|
383 NSLog(@"APPIRATER Tracking version: %@", trackingVersion); |
|
384 |
|
385 if ([trackingVersion isEqualToString:version]) |
|
386 { |
|
387 // check if the first use date has been set. if not, set it. |
|
388 NSTimeInterval timeInterval = [userDefaults doubleForKey:kAppiraterFirstUseDate]; |
|
389 if (timeInterval == 0) |
|
390 { |
|
391 timeInterval = [[NSDate date] timeIntervalSince1970]; |
|
392 [userDefaults setDouble:timeInterval forKey:kAppiraterFirstUseDate]; |
|
393 } |
|
394 |
|
395 // increment the use count |
|
396 NSInteger useCount = [userDefaults integerForKey:kAppiraterUseCount]; |
|
397 useCount++; |
|
398 [userDefaults setInteger:useCount forKey:kAppiraterUseCount]; |
|
399 if (_debug) |
|
400 NSLog(@"APPIRATER Use count: %@", @(useCount)); |
|
401 } |
|
402 else |
|
403 { |
|
404 // it's a new version of the app, so restart tracking |
|
405 [userDefaults setObject:version forKey:kAppiraterCurrentVersion]; |
|
406 [userDefaults setDouble:[[NSDate date] timeIntervalSince1970] forKey:kAppiraterFirstUseDate]; |
|
407 [userDefaults setInteger:1 forKey:kAppiraterUseCount]; |
|
408 [userDefaults setInteger:0 forKey:kAppiraterSignificantEventCount]; |
|
409 [userDefaults setBool:NO forKey:kAppiraterRatedCurrentVersion]; |
|
410 [userDefaults setBool:NO forKey:kAppiraterDeclinedToRate]; |
|
411 [userDefaults setDouble:0 forKey:kAppiraterReminderRequestDate]; |
|
412 } |
|
413 |
|
414 [userDefaults synchronize]; |
|
415 } |
|
416 |
|
417 - (void)incrementSignificantEventCount { |
|
418 // get the app's version |
|
419 NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleVersionKey]; |
|
420 |
|
421 // get the version number that we've been tracking |
|
422 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; |
|
423 NSString *trackingVersion = [userDefaults stringForKey:kAppiraterCurrentVersion]; |
|
424 if (trackingVersion == nil) |
|
425 { |
|
426 trackingVersion = version; |
|
427 [userDefaults setObject:version forKey:kAppiraterCurrentVersion]; |
|
428 } |
|
429 |
|
430 if (_debug) |
|
431 NSLog(@"APPIRATER Tracking version: %@", trackingVersion); |
|
432 |
|
433 if ([trackingVersion isEqualToString:version]) |
|
434 { |
|
435 // check if the first use date has been set. if not, set it. |
|
436 NSTimeInterval timeInterval = [userDefaults doubleForKey:kAppiraterFirstUseDate]; |
|
437 if (timeInterval == 0) |
|
438 { |
|
439 timeInterval = [[NSDate date] timeIntervalSince1970]; |
|
440 [userDefaults setDouble:timeInterval forKey:kAppiraterFirstUseDate]; |
|
441 } |
|
442 |
|
443 // increment the significant event count |
|
444 NSInteger sigEventCount = [userDefaults integerForKey:kAppiraterSignificantEventCount]; |
|
445 sigEventCount++; |
|
446 [userDefaults setInteger:sigEventCount forKey:kAppiraterSignificantEventCount]; |
|
447 if (_debug) |
|
448 NSLog(@"APPIRATER Significant event count: %@", @(sigEventCount)); |
|
449 } |
|
450 else |
|
451 { |
|
452 // it's a new version of the app, so restart tracking |
|
453 [userDefaults setObject:version forKey:kAppiraterCurrentVersion]; |
|
454 [userDefaults setDouble:0 forKey:kAppiraterFirstUseDate]; |
|
455 [userDefaults setInteger:0 forKey:kAppiraterUseCount]; |
|
456 [userDefaults setInteger:1 forKey:kAppiraterSignificantEventCount]; |
|
457 [userDefaults setBool:NO forKey:kAppiraterRatedCurrentVersion]; |
|
458 [userDefaults setBool:NO forKey:kAppiraterDeclinedToRate]; |
|
459 [userDefaults setDouble:0 forKey:kAppiraterReminderRequestDate]; |
|
460 } |
|
461 |
|
462 [userDefaults synchronize]; |
|
463 } |
|
464 |
|
465 - (void)incrementAndRate:(BOOL)canPromptForRating { |
|
466 [self incrementUseCount]; |
|
467 |
|
468 if (canPromptForRating && |
|
469 [self ratingConditionsHaveBeenMet] && |
|
470 [self ratingAlertIsAppropriate]) |
|
471 { |
|
472 dispatch_async(dispatch_get_main_queue(), |
|
473 ^{ |
|
474 [self showRatingAlert]; |
|
475 }); |
|
476 } |
|
477 } |
|
478 |
|
479 - (void)incrementSignificantEventAndRate:(BOOL)canPromptForRating { |
|
480 [self incrementSignificantEventCount]; |
|
481 |
|
482 if (canPromptForRating && |
|
483 [self ratingConditionsHaveBeenMet] && |
|
484 [self ratingAlertIsAppropriate]) |
|
485 { |
|
486 dispatch_async(dispatch_get_main_queue(), |
|
487 ^{ |
|
488 [self showRatingAlert]; |
|
489 }); |
|
490 } |
|
491 } |
|
492 |
|
493 - (BOOL)userHasDeclinedToRate { |
|
494 return [[NSUserDefaults standardUserDefaults] boolForKey:kAppiraterDeclinedToRate]; |
|
495 } |
|
496 |
|
497 - (BOOL)userHasRatedCurrentVersion { |
|
498 return [[NSUserDefaults standardUserDefaults] boolForKey:kAppiraterRatedCurrentVersion]; |
|
499 } |
|
500 |
|
501 #pragma GCC diagnostic push |
|
502 #pragma GCC diagnostic ignored "-Wdeprecated-implementations" |
|
503 + (void)appLaunched { |
|
504 [Appirater appLaunched:YES]; |
|
505 } |
|
506 #pragma GCC diagnostic pop |
|
507 |
|
508 + (void)appLaunched:(BOOL)canPromptForRating { |
|
509 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), |
|
510 ^{ |
|
511 Appirater *a = [Appirater sharedInstance]; |
|
512 if (_debug) { |
|
513 dispatch_async(dispatch_get_main_queue(), |
|
514 ^{ |
|
515 [a showRatingAlert]; |
|
516 }); |
|
517 } else { |
|
518 [a incrementAndRate:canPromptForRating]; |
|
519 } |
|
520 }); |
|
521 } |
|
522 |
|
523 - (void)hideRatingAlert { |
|
524 if (self.ratingAlert.visible) { |
|
525 if (_debug) |
|
526 NSLog(@"APPIRATER Hiding Alert"); |
|
527 [self.ratingAlert dismissWithClickedButtonIndex:-1 animated:NO]; |
|
528 } |
|
529 } |
|
530 |
|
531 + (void)appWillResignActive { |
|
532 if (_debug) |
|
533 NSLog(@"APPIRATER appWillResignActive"); |
|
534 [[Appirater sharedInstance] hideRatingAlert]; |
|
535 } |
|
536 |
|
537 + (void)appEnteredForeground:(BOOL)canPromptForRating { |
|
538 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), |
|
539 ^{ |
|
540 [[Appirater sharedInstance] incrementAndRate:canPromptForRating]; |
|
541 }); |
|
542 } |
|
543 |
|
544 + (void)userDidSignificantEvent:(BOOL)canPromptForRating { |
|
545 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), |
|
546 ^{ |
|
547 [[Appirater sharedInstance] incrementSignificantEventAndRate:canPromptForRating]; |
|
548 }); |
|
549 } |
|
550 |
|
551 #pragma GCC diagnostic push |
|
552 #pragma GCC diagnostic ignored "-Wdeprecated-implementations" |
|
553 + (void)showPrompt { |
|
554 [Appirater tryToShowPrompt]; |
|
555 } |
|
556 #pragma GCC diagnostic pop |
|
557 |
|
558 + (void)tryToShowPrompt { |
|
559 [[Appirater sharedInstance] showPromptWithChecks:true |
|
560 displayRateLaterButton:true]; |
|
561 } |
|
562 |
|
563 + (void)forceShowPrompt:(BOOL)displayRateLaterButton { |
|
564 [[Appirater sharedInstance] showPromptWithChecks:false |
|
565 displayRateLaterButton:displayRateLaterButton]; |
|
566 } |
|
567 |
|
568 - (void)showPromptWithChecks:(BOOL)withChecks |
|
569 displayRateLaterButton:(BOOL)displayRateLaterButton { |
|
570 if (withChecks == NO || [self ratingAlertIsAppropriate]) { |
|
571 [self showRatingAlert:displayRateLaterButton]; |
|
572 } |
|
573 } |
|
574 |
|
575 + (id)getRootViewController { |
|
576 UIWindow *window = [[UIApplication sharedApplication] keyWindow]; |
|
577 if (window.windowLevel != UIWindowLevelNormal) { |
|
578 NSArray *windows = [[UIApplication sharedApplication] windows]; |
|
579 for(window in windows) { |
|
580 if (window.windowLevel == UIWindowLevelNormal) { |
|
581 break; |
|
582 } |
|
583 } |
|
584 } |
|
585 |
|
586 return [Appirater iterateSubViewsForViewController:window]; // iOS 8+ deep traverse |
|
587 } |
|
588 |
|
589 + (id)iterateSubViewsForViewController:(UIView *) parentView { |
|
590 for (UIView *subView in [parentView subviews]) { |
|
591 UIResponder *responder = [subView nextResponder]; |
|
592 if([responder isKindOfClass:[UIViewController class]]) { |
|
593 return [self topMostViewController: (UIViewController *) responder]; |
|
594 } |
|
595 id found = [Appirater iterateSubViewsForViewController:subView]; |
|
596 if( nil != found) { |
|
597 return found; |
|
598 } |
|
599 } |
|
600 return nil; |
|
601 } |
|
602 |
|
603 + (UIViewController *) topMostViewController: (UIViewController *) controller { |
|
604 BOOL isPresenting = NO; |
|
605 do { |
|
606 // this path is called only on iOS 6+, so -presentedViewController is fine here. |
|
607 UIViewController *presented = [controller presentedViewController]; |
|
608 isPresenting = presented != nil; |
|
609 if(presented != nil) { |
|
610 controller = presented; |
|
611 } |
|
612 |
|
613 } while (isPresenting); |
|
614 |
|
615 return controller; |
|
616 } |
|
617 |
|
618 + (void)rateApp { |
|
619 |
|
620 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; |
|
621 [userDefaults setBool:YES forKey:kAppiraterRatedCurrentVersion]; |
|
622 [userDefaults synchronize]; |
|
623 |
|
624 //Use the in-app StoreKit view if available (iOS 6) and imported. This works in the simulator. |
|
625 if (![Appirater sharedInstance].openInAppStore && NSStringFromClass([SKStoreProductViewController class]) != nil) { |
|
626 |
|
627 SKStoreProductViewController *storeViewController = [[SKStoreProductViewController alloc] init]; |
|
628 NSNumber *appId = [NSNumber numberWithInteger:_appId.integerValue]; |
|
629 [storeViewController loadProductWithParameters:@{SKStoreProductParameterITunesItemIdentifier:appId} completionBlock:nil]; |
|
630 storeViewController.delegate = self.sharedInstance; |
|
631 |
|
632 id <AppiraterDelegate> delegate = self.sharedInstance.delegate; |
|
633 if ([delegate respondsToSelector:@selector(appiraterWillPresentModalView:animated:)]) { |
|
634 [delegate appiraterWillPresentModalView:self.sharedInstance animated:_usesAnimation]; |
|
635 } |
|
636 [[self getRootViewController] presentViewController:storeViewController animated:_usesAnimation completion:^{ |
|
637 [self setModalOpen:YES]; |
|
638 //Temporarily use a black status bar to match the StoreKit view. |
|
639 [self setStatusBarStyle:[UIApplication sharedApplication].statusBarStyle]; |
|
640 #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 |
|
641 [[UIApplication sharedApplication]setStatusBarStyle:UIStatusBarStyleLightContent animated:_usesAnimation]; |
|
642 #endif |
|
643 }]; |
|
644 |
|
645 //Use the standard openUrl method if StoreKit is unavailable. |
|
646 } else { |
|
647 |
|
648 #if TARGET_IPHONE_SIMULATOR |
|
649 NSLog(@"APPIRATER NOTE: iTunes App Store is not supported on the iOS simulator. Unable to open App Store page."); |
|
650 #else |
|
651 NSString *reviewURL = [templateReviewURL stringByReplacingOccurrencesOfString:@"APP_ID" withString:[NSString stringWithFormat:@"%@", _appId]]; |
|
652 |
|
653 // iOS 7 needs a different templateReviewURL @see https://github.com/arashpayan/appirater/issues/131 |
|
654 // Fixes condition @see https://github.com/arashpayan/appirater/issues/205 |
|
655 if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0 && [[[UIDevice currentDevice] systemVersion] floatValue] < 8.0) { |
|
656 reviewURL = [templateReviewURLiOS7 stringByReplacingOccurrencesOfString:@"APP_ID" withString:[NSString stringWithFormat:@"%@", _appId]]; |
|
657 } |
|
658 // iOS 8 needs a different templateReviewURL also @see https://github.com/arashpayan/appirater/issues/182 |
|
659 else if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) |
|
660 { |
|
661 reviewURL = [templateReviewURLiOS8 stringByReplacingOccurrencesOfString:@"APP_ID" withString:[NSString stringWithFormat:@"%@", _appId]]; |
|
662 } |
|
663 |
|
664 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:reviewURL]]; |
|
665 #endif |
|
666 } |
|
667 } |
|
668 |
|
669 - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { |
|
670 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; |
|
671 |
|
672 id <AppiraterDelegate> delegate = _delegate; |
|
673 |
|
674 switch (buttonIndex) { |
|
675 case 0: |
|
676 { |
|
677 // they don't want to rate it |
|
678 [userDefaults setBool:YES forKey:kAppiraterDeclinedToRate]; |
|
679 [userDefaults synchronize]; |
|
680 if(delegate && [delegate respondsToSelector:@selector(appiraterDidDeclineToRate:)]){ |
|
681 [delegate appiraterDidDeclineToRate:self]; |
|
682 } |
|
683 break; |
|
684 } |
|
685 case 1: |
|
686 { |
|
687 // they want to rate it |
|
688 [Appirater rateApp]; |
|
689 if(delegate&& [delegate respondsToSelector:@selector(appiraterDidOptToRate:)]){ |
|
690 [delegate appiraterDidOptToRate:self]; |
|
691 } |
|
692 break; |
|
693 } |
|
694 case 2: |
|
695 // remind them later |
|
696 [userDefaults setDouble:[[NSDate date] timeIntervalSince1970] forKey:kAppiraterReminderRequestDate]; |
|
697 [userDefaults synchronize]; |
|
698 if(delegate && [delegate respondsToSelector:@selector(appiraterDidOptToRemindLater:)]){ |
|
699 [delegate appiraterDidOptToRemindLater:self]; |
|
700 } |
|
701 break; |
|
702 default: |
|
703 break; |
|
704 } |
|
705 } |
|
706 |
|
707 //Delegate call from the StoreKit view. |
|
708 - (void)productViewControllerDidFinish:(SKStoreProductViewController *)viewController { |
|
709 [Appirater closeModal]; |
|
710 } |
|
711 |
|
712 //Close the in-app rating (StoreKit) view and restore the previous status bar style. |
|
713 + (void)closeModal { |
|
714 if (_modalOpen) { |
|
715 [[UIApplication sharedApplication]setStatusBarStyle:_statusBarStyle animated:_usesAnimation]; |
|
716 BOOL usedAnimation = _usesAnimation; |
|
717 [self setModalOpen:NO]; |
|
718 |
|
719 // get the top most controller (= the StoreKit Controller) and dismiss it |
|
720 UIViewController *presentingController = [UIApplication sharedApplication].keyWindow.rootViewController; |
|
721 presentingController = [self topMostViewController: presentingController]; |
|
722 [presentingController dismissViewControllerAnimated:_usesAnimation completion:^{ |
|
723 id <AppiraterDelegate> delegate = self.sharedInstance.delegate; |
|
724 if ([delegate respondsToSelector:@selector(appiraterDidDismissModalView:animated:)]) { |
|
725 [delegate appiraterDidDismissModalView:(Appirater *)self animated:usedAnimation]; |
|
726 } |
|
727 }]; |
|
728 [self.class setStatusBarStyle:(UIStatusBarStyle)nil]; |
|
729 } |
|
730 } |
|
731 |
|
732 @end |