FxFactory provides a simple API to allow client code to check for updates to any given product, notify the user and give her a chance to update the product. The functionality is exposed through a single function found in the FxFactoryLicensing.h
header:
void
FxFactoryShowProductUpdates(
NSString * productUUID,
NSString * productVersion,
BOOL forceCheck,
void (^ handler) (NSDictionary * response)) WEAK_IMPORT_ATTRIBUTE;
The function executes asynchronously. The only required parameters are your product’s UUID and the current version, as a string.
Users can opt out of update checking upon receiving the first notification for a new version of your product. The forceCheck
parameter lets you override this logic and request an immediate connection to our servers. This is useful for testing purposes only or if your application provides explicit UI for update checking, such as a menu command called Check for Updates….
The handler
parameter is optional. Provide a handler if your app or plug-in is interested in knowing how the request was handled. A handler that prints out the response dictionary to the Console might be useful during testing. Documentation for the various key-value pairs returned in the response dictionary is available in the header.
As for all functions in the FxFactory framework, FxFactoryShowProductUpdates
is declared as a weak symbol, and your code should check the pointer against NULL
before invoking it. This will allow your code to continue working on previous versions of FxFactory:
if (FxFactoryShowProductUpdates != NULL) {
FxFactoryShowProductUpdates( ... );
}
Unless you are forcing the request via the corresponding parameter, this function transparently handles all aspects of the process for you:
FxFactoryShowProductUpdates
API is that it handles update notifications for you. It displays alerts on the main thread only (in accordance to AppKit requirements) and may even use a separate app to display the notification if the plug-in is hosted by a child process that is unable to interact with the user. FxFactory uses NSUserDefaults
to throttle the frequency of connections to our server and to remember if the user has opted out of these notifications. If the default behavior provided by the FxFactoryShowProductUpdates
function does not fit your goals, the FxFactory framework provides a few more functions to allow you to handle the process yourself. The FxFactoryGetProductInfo
function allows your code to obtain product information from our servers. The primary purpose of this API is to allow the discovery of product updates, while giving you control over the how and when to notify the user:
void
FxFactoryGetProductInfo(
NSString * productUUID,
dispatch_queue_t queue,
void (^ handler) (NSDictionary * productInfo, NSError * error));
The function executes asynchronously. The given handler is called when the information is available or when an error has occurred. The only required parameter is your product’s UUID. When no GCD dispatch queue is provided, the handler is called on the main queue (aka the main thread of the host application).
Assuming that you have a product whose UUID is F447453A-968A-42AC-A4D0-98B0AFC15F13
, here is the starting point:
FxFactoryGetProductInfo(
@"F447453A-968A-42AC-A4D0-98B0AFC15F13",
nil,
^(NSDictionary * productInfo, NSError * error) {
if (error == nil && productInfo != nil) {
// Time to do something interesting with
// the values in productInfo:
// - Is the latest version of my product newer
// than one already installed?
// - Can the latest version of my product run
// on the current version of macOS?
// - Is the user interested in being notified
// about updates
// etc.
}
});
Assuming that a connection to our servers can be established, and that our servers know about the product identified by the given UUID, the response dictionary will contain a few key-value pairs that can be used to determine if an update is available and if the new version can run on the given system:
kFxFactoryLicensingProductLatestVersion
: a string representing the latest version of the product available through FxFactory, e.g. "6.0" or "1.0.24.3". The version string supports up to 4 numeric components. kFxFactoryLicensingProductLatestVersionRequiredOSVersion
: a string representing the minimum version of macOS required by the latest version of the product. kFxFactoryLicensingProductLatestVersionRequiredFxFactoryVersion
: a string representing the minimum version of FxFactory required by the product. This value is only matters if your product may depend on features provided by the FxFactory framework. If you follow the practice of checking for the availability of weak symbols before using them, your code may be free from checking and worrying about the version of FxFactory currently installed. An update should be advertised only if:
[[NSProcessInfo processInfo] operatingSystemVersion]
is 10.12. The fourth point can be implemented by keeping track of when a notification has been displayed, e.g. using NSUserDefaults
to store a date.
When a new version of the product is available, your code should display an alert that gives the user a few options:
FxFactoryPerformLicensingAction
to give FxFactory the responsibility of downloading and installing the new version. FxFactory will make sure that no host applications are running before the product is installed: FxFactoryPerformLicensingAction(kFxFactoryLicensingActionShow, <PRODUCT_UUID>);
It is enough to ask the product to be shown in FxFactory. On the default settings, FxFactory automatically downloads and installs product updates when the application is launched. In the event that automatic updates have been disabled, the product page will still make it obvious to the user that a new version is available, and a single click allows the user to initiate the process.
When displaying alerts through theNSAlert
API, please remember to display them on the main thread only, according to AppKit requirements. For this reasonFxFactoryGetProductInfo
schedules your handler on the main thread by default.
The "Remind me later" and "Stop checking for updates" functionality should be implemented through the use of NSUserDefaults
. If the user chooses to ignore the update for the time being, it would make sense to disable further notifications for a day.
Client code will have to compare at least two version strings in order to determine update eligibility (product version and OS version). Version strings cannot be compared using the standard methods provided by the Foundation framework. Instead, pair-wise comparison of each version component is needed. The following C function can be used as a template for comparing two version strings. It returns:
NSOrderedAscending
if the first string represents an earlier version than the second string (e.g. "1.0" and "1.0.1"). NSOrderedSame
if the two versions are equivalent (e.g. "6.0", "6.0.0"). NSOrderedDescending
if the first string represents a later version than the second string (e.g. "2.0" and "1.0"). static NSComparisonResult
MyCompareVersionStrings(
NSString * __nonnull firstString,
NSString * __nonnull secondString) {
assert(firstString != nil);
assert(secondString != nil);
NSComparisonResult result = NSOrderedSame;
NSCharacterSet * decimalDigitCharacterSet = [NSCharacterSet decimalDigitCharacterSet];
NSScanner * firstScanner = [NSScanner scannerWithString:firstString];
NSScanner * secondScanner = [NSScanner scannerWithString:secondString];
BOOL firstReachedEnd = NO;
BOOL secondReachedEnd = NO;
while ((result == NSOrderedSame) && (!firstReachedEnd || !secondReachedEnd)) {
NSUInteger firstComponent;
NSUInteger secondComponent;
NSString * decimalValue;
if (!firstReachedEnd && [firstScanner scanCharactersFromSet:decimalDigitCharacterSet intoString:& decimalValue] ) {
firstComponent = (NSUInteger) MAX(0L, [decimalValue integerValue]);
firstReachedEnd = ![firstScanner scanString:@"." intoString:nil];
} else {
firstComponent = 0;
firstReachedEnd = YES;
}
if (!secondReachedEnd && [secondScanner scanCharactersFromSet:decimalDigitCharacterSet intoString:& decimalValue] ) {
secondComponent = (NSUInteger) MAX(0L, [decimalValue integerValue]);
secondReachedEnd = ![secondScanner scanString:@"." intoString:nil];
} else {
secondComponent = 0;
secondReachedEnd = YES;
}
if (firstComponent < secondComponent) {
result = NSOrderedAscending;
} else if (firstComponent > secondComponent) {
result = NSOrderedDescending;
}
}
return result;
}
Note that the function is declared static, with the idea that the symbol will only be visible within its source file and that the symbol will not be exported by your module. If your code is loaded by a host application, it is best to export as few symbols as possible, and to ensure that public symbols do not collide with those exported by the host application or another third-party product.
Assuming that your code has called FxFactoryGetProductInfo
because it has determined that the user is actively using the product, the handler provided as an argument to the function should perform the following checks:
NSString * currentVersion = <EXTRACT YOUR PRODUCT VERSION FROM BUNDLE, OR HARDCODE IT>
NSString * latestVersion = [productInfo objectForKey:kFxFactoryLicensingProductLatestVersion];
if (currentVersion && latestVersion && MyCompareVersionStrings(currentVersion, latestVersion) == NSOrderedAscending) {
NSOperatingSystemVersion osVersion = [[NSProcessInfo processInfo] operatingSystemVersion];
NSString * currentOSVersion = [NSString stringWithFormat:@"%ld.%ld.%ld", osVersion.majorVersion, osVersion.minorVersion, osVersion.patchVersion];
NSString * requiredOSVersion = [processInfo objectForKey:kFxFactoryLicensingProductLatestVersionRequiredOSVersion];
if (requiredOSVersion && MyCompareVersionStrings(currentOSVersion, requiredOSVersion) != NSOrderedAscending ) {
// There is a newer version of our product, and the current OS version allows us to install it.
// Notify the user through an NSAlert on the main thread.
[...]
}
}
Due to the lack of namespaces in Objective-C, if you prefer to turn the sample code into a convenience method in your own category of the NSString class, remember to add a unique vendor prefix to the method’s name to reduce the chances of name-collisions, e.g.:
@implementation NSString (MyCompany)
- (NSComparisonResult)mycompany_compareToVersionString:(NSString * __nonnull)versionString {
return MyCompareVersionStrings(self, versionString);
}
@end