Checking for Product Updates

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:

  • Has the user requested product update notifications for this product?
  • Has an update check been done recently, i.e. within the past 24h?
  • Based on the data received from the servers, is a newer version of the product available? Is it compatible with the version of macOS and FxFactory installed on the system?
The great advantage of the 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.

Roll Your Own

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:

  • The system meets the minimum requirements, e.g. the product requires "10.11" and the information reported by [[NSProcessInfo processInfo] operatingSystemVersion] is 10.12.
  • The version installed is older than the one available through FxFactory.
  • The user is actively using the plug-in inside a host application.
  • Allow the user the option to disable update checks, or at least to be reminded of the update at a later time.
The third point is of particular importance: users do not enjoy having their work interrupted unless it is for a good reason. Your code should advertise a new version of the product only there is a reasonable guarantee that the user is actively using the product. For most plug-ins, it suffices to keep track of the execution of its main render method. For example, your code could initiate an update check if at least 60 frames have been rendered.

The fourth point can be implemented by keeping track of when a notification has been displayed, e.g. using NSUserDefaults to store a date.

Update Notifications with the Do-it-yourself Strategy

When a new version of the product is available, your code should display an alert that gives the user a few options:

  • Update the product now.
  • Remind me later.
  • Stop checking for updates.
When the user chooses to update the product, your code should call the 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 the NSAlert API, please remember to display them on the main thread only, according to AppKit requirements. For this reason FxFactoryGetProductInfo 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.

Comparing Version Strings

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