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

1425 lines
54 KiB

//
// 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 "-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 <Availability.h>
#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<iVersionDelegate>)delegate
{
if (_delegate == nil)
{
#if TARGET_OS_IPHONE
#define APP_CLASS UIApplication
#else
#define APP_CLASS NSApplication
#endif
_delegate = (id<iVersionDelegate>)[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];
}
- (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
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<UIAlertViewDelegate>)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.editable = 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<SKStoreProductViewControllerDelegate>)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