// // iVersion.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/iVersion // // 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 "iVersion.h" #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 "-Wundeclared-selector" #pragma clang diagnostic ignored "-Wdirect-ivar-access" #pragma clang diagnostic ignored "-Wunused-macros" #pragma clang diagnostic ignored "-Wconversion" #pragma clang diagnostic ignored "-Wselector" #pragma clang diagnostic ignored "-Wshadow" #pragma clang diagnostic ignored "-Wgnu" #import #if !__has_feature(objc_arc) #error This class requires automatic reference counting #endif NSString *const iVersionErrorDomain = @"iVersionErrorDomain"; NSString *const iVersionInThisVersionTitleKey = @"iVersionInThisVersionTitle"; NSString *const iVersionUpdateAvailableTitleKey = @"iVersionUpdateAvailableTitle"; NSString *const iVersionVersionLabelFormatKey = @"iVersionVersionLabelFormat"; NSString *const iVersionOKButtonKey = @"iVersionOKButton"; NSString *const iVersionIgnoreButtonKey = @"iVersionIgnoreButton"; NSString *const iVersionRemindButtonKey = @"iVersionRemindButton"; NSString *const iVersionDownloadButtonKey = @"iVersionDownloadButton"; static NSString *const iVersionAppStoreIDKey = @"iVersionAppStoreID"; static NSString *const iVersionLastVersionKey = @"iVersionLastVersionChecked"; static NSString *const iVersionIgnoreVersionKey = @"iVersionIgnoreVersion"; static NSString *const iVersionLastCheckedKey = @"iVersionLastChecked"; static NSString *const iVersionLastRemindedKey = @"iVersionLastReminded"; static NSString *const iVersionMacAppStoreBundleID = @"com.apple.appstore"; static NSString *const iVersionAppLookupURLFormat = @"http://itunes.apple.com/%@/lookup"; static NSString *const iVersioniOSAppStoreURLFormat = @"itms-apps://itunes.apple.com/app/id%@"; static NSString *const iVersionMacAppStoreURLFormat = @"macappstore://itunes.apple.com/app/id%@"; #define SECONDS_IN_A_DAY 86400.0 #define MAC_APP_STORE_REFRESH_DELAY 5.0 #define REQUEST_TIMEOUT 60.0 @implementation NSString(iVersion) - (NSComparisonResult)compareVersion:(NSString *)version { return [self compare:version options:NSNumericSearch]; } - (NSComparisonResult)compareVersionDescending:(NSString *)version { return (NSComparisonResult)(0 - [self compareVersion:version]); } @end @interface iVersion () @property (nonatomic, copy) NSDictionary *remoteVersionsDict; @property (nonatomic, strong) NSError *downloadError; @property (nonatomic, copy) NSString *versionDetails; @property (nonatomic, strong) id visibleLocalAlert; @property (nonatomic, strong) id visibleRemoteAlert; @property (nonatomic, assign) BOOL checkingForNewVersion; @end @implementation iVersion + (void)load { [self performSelectorOnMainThread:@selector(sharedInstance) withObject:nil waitUntilDone:NO]; } + (iVersion *)sharedInstance { static iVersion *sharedInstance = nil; if (sharedInstance == nil) { sharedInstance = [[iVersion alloc] init]; } return sharedInstance; } - (NSString *)localizedStringForKey:(NSString *)key withDefault:(NSString *)defaultString { static NSBundle *bundle = nil; if (bundle == nil) { NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"iVersion" 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]; } - (iVersion *)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]; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didRotate) name:UIDeviceOrientationDidChangeNotification object:nil]; #endif //get country self.appStoreCountry = [(NSLocale *)[NSLocale currentLocale] objectForKey:NSLocaleCountryCode]; if ([self.appStoreCountry isEqualToString:@"150"]) { self.appStoreCountry = @"eu"; } else if ([self.appStoreCountry stringByReplacingOccurrencesOfString:@"[A-Za-z]{2}" withString:@"" 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]; } //bundle id self.applicationBundleID = [NSBundle mainBundle].bundleIdentifier; //default settings self.updatePriority = iVersionUpdatePriorityDefault; self.useAllAvailableLanguages = YES; self.onlyPromptIfMainWindowIsAvailable = YES; self.checkAtLaunch = YES; self.checkPeriod = 0.0f; self.remindPeriod = 1.0f; #ifdef DEBUG //enable verbose logging in debug mode self.verboseLogging = YES; #endif //app launched [self performSelectorOnMainThread:@selector(applicationLaunched) withObject:nil waitUntilDone:NO]; } return self; } - (id)delegate { if (_delegate == nil) { #if TARGET_OS_IPHONE #define APP_CLASS UIApplication #else #define APP_CLASS NSApplication #endif _delegate = (id)[APP_CLASS sharedApplication].delegate; } return _delegate; } - (NSString *)inThisVersionTitle { return _inThisVersionTitle ?: [self localizedStringForKey:iVersionInThisVersionTitleKey withDefault:@"New in this version"]; } - (NSString *)updateAvailableTitle { return _updateAvailableTitle ?: [self localizedStringForKey:iVersionUpdateAvailableTitleKey withDefault:@"New version available"]; } - (NSString *)versionLabelFormat { return _versionLabelFormat ?: [self localizedStringForKey:iVersionVersionLabelFormatKey withDefault:@"Version %@"]; } - (NSString *)okButtonLabel { return _okButtonLabel ?: [self localizedStringForKey:iVersionOKButtonKey withDefault:@"OK"]; } - (NSString *)ignoreButtonLabel { return _ignoreButtonLabel ?: [self localizedStringForKey:iVersionIgnoreButtonKey withDefault:@"Ignore"]; } - (NSString *)downloadButtonLabel { return _downloadButtonLabel ?: [self localizedStringForKey:iVersionDownloadButtonKey withDefault:@"Download"]; } - (NSString *)remindButtonLabel { return _remindButtonLabel ?: [self localizedStringForKey:iVersionRemindButtonKey withDefault:@"Remind Me Later"]; } - (NSURL *)updateURL { if (_updateURL) { return _updateURL; } if (!self.appStoreID) { NSLog(@"iVersion error: No App Store ID was found for this application. If the application is not intended for App Store release then you must specify a custom updateURL."); } #if TARGET_OS_IPHONE return [NSURL URLWithString:[NSString stringWithFormat:iVersioniOSAppStoreURLFormat, @(self.appStoreID)]]; #else return [NSURL URLWithString:[NSString stringWithFormat:iVersionMacAppStoreURLFormat, @(self.appStoreID)]]; #endif } - (NSUInteger)appStoreID { return [[[NSUserDefaults standardUserDefaults] objectForKey:iVersionAppStoreIDKey] unsignedIntegerValue]; } - (void)setAppStoreID:(NSUInteger)appStoreID { [[NSUserDefaults standardUserDefaults] setInteger:(NSInteger)appStoreID forKey:iVersionAppStoreIDKey]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (NSDate *)lastChecked { return [[NSUserDefaults standardUserDefaults] objectForKey:iVersionLastCheckedKey]; } - (void)setLastChecked:(NSDate *)date { [[NSUserDefaults standardUserDefaults] setObject:date forKey:iVersionLastCheckedKey]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (NSDate *)lastReminded { return [[NSUserDefaults standardUserDefaults] objectForKey:iVersionLastRemindedKey]; } - (void)setLastReminded:(NSDate *)date { [[NSUserDefaults standardUserDefaults] setObject:date forKey:iVersionLastRemindedKey]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (NSString *)ignoredVersion { return [[NSUserDefaults standardUserDefaults] objectForKey:iVersionIgnoreVersionKey]; } - (void)setIgnoredVersion:(NSString *)version { [[NSUserDefaults standardUserDefaults] setObject:version forKey:iVersionIgnoreVersionKey]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (BOOL)viewedVersionDetails { return [[[NSUserDefaults standardUserDefaults] objectForKey:iVersionLastVersionKey] isEqualToString:self.applicationVersion]; } - (void)setViewedVersionDetails:(BOOL)viewed { [[NSUserDefaults standardUserDefaults] setObject:(viewed? self.applicationVersion: nil) forKey:iVersionLastVersionKey]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (NSString *)lastVersion { return [[NSUserDefaults standardUserDefaults] objectForKey:iVersionLastVersionKey]; } - (void)setLastVersion:(NSString *)version { [[NSUserDefaults standardUserDefaults] setObject:version forKey:iVersionLastVersionKey]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (NSDictionary *)localVersionsDict { static NSDictionary *versionsDict = nil; if (versionsDict == nil) { if (self.localVersionsPlistPath == nil) { versionsDict = [[NSDictionary alloc] init]; //empty dictionary } else { NSString *versionsFile = [[NSBundle mainBundle].resourcePath stringByAppendingPathComponent:self.localVersionsPlistPath]; versionsDict = [[NSDictionary alloc] initWithContentsOfFile:versionsFile]; if (!versionsDict) { // Get the path to versions plist in localized directory NSArray *pathComponents = [self.localVersionsPlistPath componentsSeparatedByString:@"."]; versionsFile = (pathComponents.count == 2) ? [[NSBundle mainBundle] pathForResource:pathComponents[0] ofType:pathComponents[1]] : nil; versionsDict = [[NSDictionary alloc] initWithContentsOfFile:versionsFile]; } } } return versionsDict; } - (NSString *)mostRecentVersionInDict:(NSDictionary *)dict { return [dict.allKeys sortedArrayUsingSelector:@selector(compareVersion:)].lastObject; } - (NSString *)versionDetails:(NSString *)version inDict:(NSDictionary *)dict { id versionData = dict[version]; if ([versionData isKindOfClass:[NSString class]]) { return versionData; } else if ([versionData isKindOfClass:[NSArray class]]) { return [versionData componentsJoinedByString:@"\n"]; } return nil; } - (NSString *)versionDetailsSince:(NSString *)lastVersion inDict:(NSDictionary *)dict { if (self.previewMode) { lastVersion = @"0"; } BOOL newVersionFound = NO; NSMutableString *details = [NSMutableString string]; NSArray *versions = [dict.allKeys sortedArrayUsingSelector:@selector(compareVersionDescending:)]; for (NSString *version in versions) { if ([version compareVersion:lastVersion] == NSOrderedDescending) { newVersionFound = YES; if (self.groupNotesByVersion) { [details appendString:[self.versionLabelFormat stringByReplacingOccurrencesOfString:@"%@" withString:version]]; [details appendString:@"\n\n"]; } [details appendString:[self versionDetails:version inDict:dict] ?: @""]; [details appendString:@"\n"]; if (self.groupNotesByVersion) { [details appendString:@"\n"]; } } } return newVersionFound? [details stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]: nil; } - (NSString *)versionDetails { if (!_versionDetails) { if (self.viewedVersionDetails) { self.versionDetails = [self versionDetails:self.applicationVersion inDict:[self localVersionsDict]]; } else { self.versionDetails = [self versionDetailsSince:self.lastVersion inDict:[self localVersionsDict]]; } } return _versionDetails; } - (NSString *)URLEncodedString:(NSString *)string { CFStringRef stringRef = CFBridgingRetain(string); CFStringRef encoded = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, stringRef, NULL, CFSTR("!*'\"();:@&=+$,/?%#[]% "), kCFStringEncodingUTF8); CFRelease(stringRef); return CFBridgingRelease(encoded); } - (void)downloadedVersionsData { #if !TARGET_OS_IPHONE //only show when main window is available if (self.onlyPromptIfMainWindowIsAvailable && ![NSApplication sharedApplication].mainWindow) { [self performSelector:@selector(downloadedVersionsData) withObject:nil afterDelay:0.5]; return; } #endif if (self.checkingForNewVersion) { //no longer checking self.checkingForNewVersion = NO; //check if data downloaded if (!self.remoteVersionsDict) { //log the error if (self.downloadError) { NSLog(@"iVersion update check failed because: %@", (self.downloadError).localizedDescription); } else { NSLog(@"iVersion update check failed because an unknown error occured"); } if ([self.delegate respondsToSelector:@selector(iVersionVersionCheckDidFailWithError:)]) { [self.delegate iVersionVersionCheckDidFailWithError:self.downloadError]; } //deprecated code path else if ([self.delegate respondsToSelector:@selector(iVersionVersionCheckFailed:)]) { NSLog(@"iVersionVersionCheckFailed: delegate method is deprecated, use iVersionVersionCheckDidFailWithError: instead"); [self.delegate performSelector:@selector(iVersionVersionCheckFailed:) withObject:self.downloadError]; } return; } //get version details NSString *details = [self versionDetailsSince:self.applicationVersion inDict:self.remoteVersionsDict]; NSString *mostRecentVersion = [self mostRecentVersionInDict:self.remoteVersionsDict]; if (details) { //inform delegate of new version if ([self.delegate respondsToSelector:@selector(iVersionDidDetectNewVersion:details:)]) { [self.delegate iVersionDidDetectNewVersion:mostRecentVersion details:details]; } //deprecated code path else if ([self.delegate respondsToSelector:@selector(iVersionDetectedNewVersion:details:)]) { NSLog(@"iVersionDetectedNewVersion:details: delegate method is deprecated, use iVersionDidDetectNewVersion:details: instead"); [self.delegate performSelector:@selector(iVersionDetectedNewVersion:details:) withObject:mostRecentVersion withObject:details]; } //check if ignored BOOL showDetails = ![self.ignoredVersion isEqualToString:mostRecentVersion] || self.previewMode; if (showDetails) { if ([self.delegate respondsToSelector:@selector(iVersionShouldDisplayNewVersion:details:)]) { showDetails = [self.delegate iVersionShouldDisplayNewVersion:mostRecentVersion details:details]; if (!showDetails && self.verboseLogging) { NSLog(@"iVersion did not display the new version because the iVersionShouldDisplayNewVersion:details: delegate method returned NO"); } } } else if (self.verboseLogging) { NSLog(@"iVersion did not display the new version because it was marked as ignored"); } //show details if (showDetails && !self.visibleRemoteAlert) { NSString *title = self.updateAvailableTitle; if (!self.groupNotesByVersion) { title = [title stringByAppendingFormat:@" (%@)", mostRecentVersion]; } self.visibleRemoteAlert = [self showAlertWithTitle:title details:details defaultButton:self.downloadButtonLabel ignoreButton:[self showIgnoreButton]? self.ignoreButtonLabel: nil remindButton:[self showRemindButton]? self.remindButtonLabel: nil]; } } else if ([self.delegate respondsToSelector:@selector(iVersionDidNotDetectNewVersion)]) { [self.delegate iVersionDidNotDetectNewVersion]; } } } - (BOOL)shouldCheckForNewVersion { //debug mode? if (!self.previewMode) { //check if within the reminder period if (self.lastReminded != nil) { //reminder takes priority over check period if ([[NSDate date] timeIntervalSinceDate:self.lastReminded] < self.remindPeriod * SECONDS_IN_A_DAY) { if (self.verboseLogging) { NSLog(@"iVersion did not check for a new version because the user last asked to be reminded less than %g days ago", self.remindPeriod); } return NO; } } //check if within the check period else if (self.lastChecked != nil && [[NSDate date] timeIntervalSinceDate:self.lastChecked] < self.checkPeriod * SECONDS_IN_A_DAY) { if (self.verboseLogging) { NSLog(@"iVersion did not check for a new version because the last check was less than %g days ago", self.checkPeriod); } return NO; } } else if (self.verboseLogging) { NSLog(@"iVersion debug mode is enabled - make sure you disable this for release"); } //confirm with delegate if ([self.delegate respondsToSelector:@selector(iVersionShouldCheckForNewVersion)]) { BOOL shouldCheck = [self.delegate iVersionShouldCheckForNewVersion]; if (!shouldCheck && self.verboseLogging) { NSLog(@"iVersion did not check for a new version because the iVersionShouldCheckForNewVersion delegate method returned NO"); } return shouldCheck; } //perform the check 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 { self.appStoreID = appStoreIDString.longLongValue; } - (void)checkForNewVersionInBackground { @synchronized (self) { @autoreleasepool { BOOL newerVersionAvailable = NO; BOOL osVersionSupported = NO; NSString *latestVersion = nil; NSDictionary *versions = nil; //first check iTunes NSString *iTunesServiceURL = [NSString stringWithFormat:iVersionAppLookupURLFormat, self.appStoreCountry]; if (self.appStoreID) { iTunesServiceURL = [iTunesServiceURL stringByAppendingFormat:@"?id=%@", @(self.appStoreID)]; } else { iTunesServiceURL = [iTunesServiceURL stringByAppendingFormat:@"?bundleId=%@", self.applicationBundleID]; } if (self.verboseLogging) { NSLog(@"iVersion is checking %@ for a new app version...", iTunesServiceURL); } NSError *error = nil; NSURLResponse *response = nil; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:iTunesServiceURL] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData 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 supported OS version NSString *minimumSupportedOSVersion = [self valueForKey:@"minimumOsVersion" inJSON:json]; #if TARGET_OS_IPHONE NSString *systemVersion = [UIDevice currentDevice].systemVersion; #else NSString *systemVersion = nil; #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1100 if ([[NSProcessInfo class] respondsToSelector:@selector(processInfo)]) { NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion; systemVersion = [NSString stringWithFormat:@"%zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion]; } else #endif { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" SInt32 majorVersion = 0, minorVersion = 0, patchVersion = 0; Gestalt(gestaltSystemVersionMajor, &majorVersion); Gestalt(gestaltSystemVersionMinor, &minorVersion); Gestalt(gestaltSystemVersionBugFix, &patchVersion); systemVersion = [NSString stringWithFormat:@"%d.%d.%d", majorVersion, minorVersion, patchVersion]; #pragma clang diagnostic pop } #endif osVersionSupported = ([systemVersion compare:minimumSupportedOSVersion options:NSNumericSearch] != NSOrderedAscending); if (!osVersionSupported) { error = [NSError errorWithDomain:iVersionErrorDomain code:iVersionErrorOSVersionNotSupported userInfo:@{NSLocalizedDescriptionKey: @"Current OS version is not supported."}]; } //get version details NSString *releaseNotes = [self valueForKey:@"releaseNotes" inJSON:json]; latestVersion = [self valueForKey:@"version" inJSON:json]; if (latestVersion && osVersionSupported) { versions = @{latestVersion: releaseNotes ?: @""}; } //get app id if (!self.appStoreID) { NSString *appStoreIDString = [self valueForKey:@"trackId" inJSON:json]; [self performSelectorOnMainThread:@selector(setAppStoreIDOnMainThread:) withObject:appStoreIDString waitUntilDone:YES]; if (self.verboseLogging) { NSLog(@"iVersion found the app on iTunes. The App Store ID is %@", appStoreIDString); } } //check for new version newerVersionAvailable = ([latestVersion compareVersion:self.applicationVersion] == NSOrderedDescending); if (self.verboseLogging) { if (newerVersionAvailable) { NSLog(@"iVersion found a new version (%@) of the app on iTunes. Current version is %@", latestVersion, self.applicationVersion); } else { NSLog(@"iVersion did not find a new version of the app on iTunes. Current version is %@, latest version is %@", self.applicationVersion, latestVersion); } } } else { if (self.verboseLogging) { NSLog(@"iVersion 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:iVersionErrorDomain code:iVersionErrorBundleIdDoesNotMatchAppStore userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Application bundle ID does not match expected value of %@", bundleID]}]; } } else if (self.appStoreID || !self.remoteVersionsPlistURL) { if (self.verboseLogging) { NSLog(@"iVersion could not find this application on iTunes. If your app is not intended for App Store release then you must specify a remoteVersionsPlistURL. If this is the first release of your application then it's not a problem that it cannot be found on the store yet"); } error = [NSError errorWithDomain:iVersionErrorDomain code:iVersionErrorApplicationNotFoundOnAppStore userInfo:@{NSLocalizedDescriptionKey: @"The application could not be found on the App Store."}]; } else if (!self.appStoreID && self.verboseLogging) { NSLog(@"iVersion 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"); } //now check plist for alternative release notes if (((self.appStoreID && newerVersionAvailable && osVersionSupported) || !self.appStoreID || self.previewMode) && self.remoteVersionsPlistURL) { if (self.verboseLogging) { NSLog(@"iVersion will check %@ for %@", self.remoteVersionsPlistURL, self.appStoreID? @"release notes": @"a new app version"); } NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.remoteVersionsPlistURL] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:REQUEST_TIMEOUT]; NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; if (data) { NSPropertyListFormat format; NSDictionary *plistVersions = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:&format error:&error]; if (latestVersion) { //remove versions that are greater than latest in app store NSMutableDictionary *versions = [NSMutableDictionary dictionary]; for (NSString *version in plistVersions) { if ([version compareVersion:latestVersion] != NSOrderedDescending) { versions[version] = plistVersions[version]; } } plistVersions = versions; } if (!latestVersion || plistVersions[latestVersion] || !_useAppStoreDetailsIfNoPlistEntryFound) { versions = [plistVersions copy]; } } else if (self.verboseLogging) { NSLog(@"iVersion was unable to download the user-specified release notes"); } } } 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}]; } } [self performSelectorOnMainThread:@selector(setDownloadError:) withObject:error waitUntilDone:YES]; [self performSelectorOnMainThread:@selector(setRemoteVersionsDict:) withObject:versions waitUntilDone:YES]; [self performSelectorOnMainThread:@selector(setLastChecked:) withObject:[NSDate date] waitUntilDone:YES]; [self performSelectorOnMainThread:@selector(downloadedVersionsData) withObject:nil waitUntilDone:YES]; } } } - (void)checkForNewVersion { if (!self.checkingForNewVersion) { self.checkingForNewVersion = YES; [self performSelectorInBackground:@selector(checkForNewVersionInBackground) withObject:nil]; } } - (void)checkIfNewVersion { #if !TARGET_OS_IPHONE //only show when main window is available if (self.onlyPromptIfMainWindowIsAvailable && ![NSApplication sharedApplication].mainWindow) { [self performSelector:@selector(checkIfNewVersion) withObject:nil afterDelay:0.5]; return; } #endif if (self.lastVersion != nil || self.showOnFirstLaunch || self.previewMode) { if ([self.applicationVersion compareVersion:self.lastVersion] == NSOrderedDescending || self.previewMode) { //clear reminder self.lastReminded = nil; //get version details BOOL showDetails = !!self.versionDetails; if (showDetails && [self.delegate respondsToSelector:@selector(iVersionShouldDisplayCurrentVersionDetails:)]) { showDetails = [self.delegate iVersionShouldDisplayCurrentVersionDetails:self.versionDetails]; } //show details if (showDetails && !self.visibleLocalAlert && !self.visibleRemoteAlert) { self.visibleLocalAlert = [self showAlertWithTitle:self.inThisVersionTitle details:self.versionDetails defaultButton:self.okButtonLabel ignoreButton:nil remindButton:nil]; } } } else { //record this as last viewed release self.viewedVersionDetails = YES; } } - (BOOL)showIgnoreButton { return (self.ignoreButtonLabel).length && self.updatePriority < iVersionUpdatePriorityMedium; } - (BOOL)showRemindButton { return (self.remindButtonLabel).length && self.updatePriority < iVersionUpdatePriorityHigh; } - (id)showAlertWithTitle:(NSString *)title details:(NSString *)details defaultButton:(NSString *)defaultButton ignoreButton:(NSString *)ignoreButton remindButton:(NSString *)remindButton { #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:title message:details preferredStyle:UIAlertControllerStyleAlert]; //download/ok action [alert addAction:[UIAlertAction actionWithTitle:self.downloadButtonLabel style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { [self didDismissAlert:alert withButtonAtIndex:0]; }]]; //ignore action if ([self showIgnoreButton]) { [alert addAction:[UIAlertAction actionWithTitle:self.ignoreButtonLabel 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 showIgnoreButton]? 2: 1]; }]]; } //get current view controller and present alert [topController presentViewController:alert animated:YES completion:NULL]; return alert; } else { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:details delegate:(id)self cancelButtonTitle:nil otherButtonTitles:defaultButton, nil]; if (ignoreButton) { [alert addButtonWithTitle:ignoreButton]; alert.cancelButtonIndex = 1; } if (remindButton) { [alert addButtonWithTitle:remindButton]; } [alert show]; return alert; } #else NSAlert *alert = [[NSAlert alloc] init]; alert.messageText = title; alert.informativeText = self.inThisVersionTitle; [alert addButtonWithTitle:defaultButton]; NSScrollView *scrollview = [[NSScrollView alloc] initWithFrame:NSMakeRect(0.0, 0.0, 380.0, 15.0)]; NSSize contentSize = scrollview.contentSize; scrollview.borderType = NSBezelBorder; scrollview.hasVerticalScroller = YES; scrollview.hasHorizontalScroller = NO; scrollview.autoresizingMask = (NSAutoresizingMaskOptions)(NSViewWidthSizable|NSViewHeightSizable); NSTextView *textView = [[NSTextView alloc] initWithFrame:NSMakeRect(0.0, 0.0, contentSize.width, contentSize.height)]; textView.minSize = NSMakeSize(0.0, contentSize.height); textView.maxSize = NSMakeSize(FLT_MAX, FLT_MAX); textView.verticallyResizable = YES; textView.horizontallyResizable = NO; textView.autoresizingMask = NSViewWidthSizable; textView.textContainer.containerSize = NSMakeSize(contentSize.width, FLT_MAX); textView.textContainer.widthTracksTextView = YES; textView.string = details; scrollview.documentView = textView; [textView sizeToFit]; CGFloat height = MIN(200.0, [[scrollview documentView] frame].size.height) + 3.0; scrollview.frame = NSMakeRect(0.0, 0.0, scrollview.frame.size.width, height); alert.accessoryView = scrollview; if (ignoreButton) { [alert addButtonWithTitle:ignoreButton]; } if (remindButton) { [alert addButtonWithTitle:remindButton]; } #if __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9 if (![alert respondsToSelector:@selector(runModal:)]) { NSModalResponse modalResponse = [alert runModal]; if (modalResponse == NSAlertFirstButtonReturn) { //right most button [self didDismissAlert:alert withButtonAtIndex:0]; } else if (modalResponse == NSAlertSecondButtonReturn) { [self didDismissAlert:alert withButtonAtIndex:1]; } else { [self didDismissAlert:alert withButtonAtIndex:2]; } } else #endif { NSModalResponse modalResponse = [alert runModal]; if (modalResponse == NSAlertFirstButtonReturn) { //right most button [self didDismissAlert:alert withButtonAtIndex:0]; } else if (modalResponse == NSAlertSecondButtonReturn) { [self didDismissAlert:alert withButtonAtIndex:1]; } else { [self didDismissAlert:alert withButtonAtIndex:2]; } } return alert; #endif } - (void)didDismissAlert:(id)alertView withButtonAtIndex:(NSInteger)buttonIndex { //get button indices NSInteger downloadButtonIndex = 0; NSInteger ignoreButtonIndex = [self showIgnoreButton]? 1: 0; NSInteger remindButtonIndex = [self showRemindButton]? ignoreButtonIndex + 1: 0; //latest version NSString *latestVersion = [self mostRecentVersionInDict:self.remoteVersionsDict]; if (alertView == self.visibleLocalAlert) { //record that details have been viewed self.viewedVersionDetails = YES; //release alert self.visibleLocalAlert = nil; return; } if (buttonIndex == downloadButtonIndex) { //clear reminder self.lastReminded = nil; //log event if ([self.delegate respondsToSelector:@selector(iVersionUserDidAttemptToDownloadUpdate:)]) { [self.delegate iVersionUserDidAttemptToDownloadUpdate:latestVersion]; } if (![self.delegate respondsToSelector:@selector(iVersionShouldOpenAppStore)] || [self.delegate iVersionShouldOpenAppStore]) { //go to download page [self openAppPageInAppStore]; } } else if (buttonIndex == ignoreButtonIndex) { //ignore this version self.ignoredVersion = latestVersion; self.lastReminded = nil; //log event if ([self.delegate respondsToSelector:@selector(iVersionUserDidIgnoreUpdate:)]) { [self.delegate iVersionUserDidIgnoreUpdate:latestVersion]; } } else if (buttonIndex == remindButtonIndex) { //remind later self.lastReminded = [NSDate date]; //log event if ([self.delegate respondsToSelector:@selector(iVersionUserDidRequestReminderForUpdate:)]) { [self.delegate iVersionUserDidRequestReminderForUpdate:latestVersion]; } } //release alert self.visibleRemoteAlert = nil; } #if TARGET_OS_IPHONE - (BOOL)openAppPageInAppStore { if (!_updateURL && !self.appStoreID) { if (self.verboseLogging) { NSLog(@"iVersion was unable to open the App Store because the app store ID is not set."); } return NO; } #if defined(IVERSION_USE_STOREKIT) && IVERSION_USE_STOREKIT if (!_updateURL && [SKStoreProductViewController class]) { if (self.verboseLogging) { NSLog(@"iVersion will attempt to open the StoreKit in-app product page using the following app store ID: %@", @(self.appStoreID)); } //create store view controller SKStoreProductViewController *productController = [[SKStoreProductViewController alloc] init]; productController.delegate = (id)self; //load product details NSDictionary *productParameters = @{SKStoreProductParameterITunesItemIdentifier: [@(_appStoreID) description]}; [productController loadProductWithParameters:productParameters completionBlock:NULL]; //get root view controller UIWindow *window = [[UIApplication sharedApplication] delegate].window; UIViewController *rootViewController = window.rootViewController; if (!rootViewController) { if (self.verboseLogging) { NSLog(@"iVersion couldn't find a root view controller from which to display StoreKit product page"); } } else { while (rootViewController.presentedViewController) { rootViewController = rootViewController.presentedViewController; } //present product view controller [rootViewController presentViewController:productController animated:YES completion:nil]; if ([self.delegate respondsToSelector:@selector(iVersionDidPresentStoreKitModal)]) { [self.delegate iVersionDidPresentStoreKitModal]; } return YES; } } #endif if (self.verboseLogging) { NSLog(@"iVersion will open the App Store using the following URL: %@", self.updateURL); } [[UIApplication sharedApplication] openURL:self.updateURL]; return YES; } - (void)productViewControllerDidFinish:(UIViewController *)controller { [controller.presentingViewController dismissViewControllerAnimated:YES completion:NULL]; if ([self.delegate respondsToSelector:@selector(iVersionDidDismissStoreKitModal)]) { [self.delegate iVersionDidDismissStoreKitModal]; } } - (void)resizeAlertView:(UIAlertView *)alertView { if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation) && [[UIDevice currentDevice].systemVersion floatValue] < 7.0f) { CGFloat max = alertView.window.bounds.size.height - alertView.frame.size.height - 10.0f; CGFloat offset = 0.0f; for (UIView *view in alertView.subviews) { CGRect frame = view.frame; if ([view isKindOfClass:[UILabel class]]) { UILabel *label = (UILabel *)view; if ([label.text isEqualToString:alertView.message]) { label.lineBreakMode = NSLineBreakByWordWrapping; label.numberOfLines = 0; label.alpha = 1.0f; [label sizeToFit]; offset = label.frame.size.height - frame.size.height; frame.size.height = label.frame.size.height; if (offset > max) { frame.size.height -= (offset - max); offset = max; } if (offset > max - 10.0f) { frame.size.height -= (offset - max - 10); frame.origin.y += (offset - max - 10) / 2.0f; } } } else if ([view isKindOfClass:[UITextView class]]) { view.alpha = 0.0f; } else if ([view isKindOfClass:[UIControl class]]) { frame.origin.y += offset; } view.frame = frame; } CGRect frame = alertView.frame; frame.origin.y -= roundf(offset/2.0f); frame.size.height += offset; alertView.frame = frame; } } - (void)didRotate { [self performSelectorOnMainThread:@selector(resizeAlertView:) withObject:self.visibleLocalAlert waitUntilDone:NO]; [self performSelectorOnMainThread:@selector(resizeAlertView:) withObject:self.visibleRemoteAlert waitUntilDone:NO]; } - (void)willPresentAlertView:(UIAlertView *)alertView { [self resizeAlertView:alertView]; } - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { [self didDismissAlert:alertView withButtonAtIndex:buttonIndex]; } #else - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(__unused void *)contextInfo { [self didDismissAlert:alert withButtonAtIndex:returnCode - NSAlertFirstButtonReturn]; } - (void)openAppPageWhenAppStoreLaunched { //check if app store is running for (NSRunningApplication *app in [NSWorkspace sharedWorkspace].runningApplications) { if ([app.bundleIdentifier isEqualToString:iVersionMacAppStoreBundleID]) { //open app page [[NSWorkspace sharedWorkspace] performSelector:@selector(openURL:) withObject:self.updateURL afterDelay:MAC_APP_STORE_REFRESH_DELAY]; return; } } //try again [self performSelector:@selector(openAppPageWhenAppStoreLaunched) withObject:nil afterDelay:0.0]; } - (BOOL)openAppPageInAppStore { if (!_updateURL && !self.appStoreID) { if (self.verboseLogging) { NSLog(@"iVersion was unable to open the App Store because the app store ID is not set."); } return NO; } if (self.verboseLogging) { NSLog(@"iVersion will open the App Store using the following URL: %@", self.updateURL); } [[NSWorkspace sharedWorkspace] openURL:self.updateURL]; if (!_updateURL) [self openAppPageWhenAppStoreLaunched]; return YES; } #endif - (void)applicationLaunched { if (self.checkAtLaunch) { [self checkIfNewVersion]; if ([self shouldCheckForNewVersion]) [self checkForNewVersion]; } else if (self.verboseLogging) { NSLog(@"iVersion will not check for updates because the checkAtLaunch option is disabled"); } } #if TARGET_OS_IPHONE - (void)applicationWillEnterForeground { if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { if (self.checkAtLaunch) { if ([self shouldCheckForNewVersion]) [self checkForNewVersion]; } else if (self.verboseLogging) { NSLog(@"iVersion will not check for updates because the checkAtLaunch option is disabled"); } } } #endif @end