Lite To Paid iPhone Application Data Migrations With Custom URL Handlers

By
On June 29, 2009
Stephen Lombardo and Zetetic are the creators of the encrypted iPhone data vault Strip.

Apple enforces a number of restrictions on applications in the App Store. Among the most painful is the lack of feature-limited trials. Applications are either completely free, or the customer must pay up front, sight unseen. The proliferation of “Lite” applications is a direct result of this shortcoming. Publishers will often create two application versions: the first is fully functional but costs money, the second is “Lite” and comes with limited features but zero price tag. The goal is for prospective customers to try the free version first and then decide to buy the paid full version separately if they like it.

Because free applications have greater exposure this approach is widely advocated as a means to increase visibility and grow sales. When building a game or other stateless application the approach makes complete sense. However, utility applications often maintain information entered by the device owner. Application authors are faced with a dilemma because the iPhone’s security sandbox prevents one application from reading another application’s files. Thus, when customers upgrade from the Lite application they are penalized by having to re-enter all data!

When we developed our data vault product, Strip, we felt this situation was unacceptable. We wanted to offer Strip Lite, free for people to try, limited to 10 entries. Upgrading customers should be able to import their data from the Lite version. No pain.

The Approach

Strip stores user data in an encrypted SQLite Database. Since we really wanted a way to copy the data file from Strip Lite to Strip we developed a fairly clever solution: use a registered URL handler as a means of communication between application versions. This is how it works:

  1. A customer downloads Strip Lite for free and enters some unique private data. All information is written to a database in the application’s document directory.
  2. When the 10 record limit is hit Strip Lite asks them to upgrade. An upgrade button launches to explain the process and allow the user to download the paid version of the product.
  3. Once Strip is purchased and installed, our customer will launch Strip Lite and click an “Export to Strip” button. Strip Lite reads the application database into an NSData object, Base64 encodes it and then Launches a “strip://” URL with the encoded file contents as a URL parameter.
  4. The full version of Strip is registered as a handler for “strip://” URLs. The OS launches Strip and passed the URL to the handleOpenURL method of the application delegate. It parses and decodes the data and writes the database to the desired location in the new applications Document folder.
  5. Our customer now has access to all the data the originally entered into Strip Lite!

Project Setup

This tutorial assumes that your project is already configured with two targets, one for your full version, and one for the Lite.

Start by creating a new InfoLite.plist file for your Lite version. We use 2 separate .plist files to ensure that the Full version is the only registered URL handler (we don’t want the Lite version responding to upgrade events itself!). It is easiest to just make a copy of the existing Info.plist file named InfoLite.plist.

Next, Open up the Build preferences for the Lite target. Change the Info.plist File property to read InfoLite.plist. This will ensure that the Lite target includes the proper file.

Now open the Info.plist (used by the Full Version) and register a custom URL handler. This is a straightforward process of adding elements as shown in the following screenshot.

Please note that the URL identifier and URL scheme must be unique for your application — see this stepwise overview of the process for reference.

Finally, the application must include code to perform Base64 encoding itself. This functionality is, unfortunately, not directly accessible in the framework SDK. There are a few options in Cocoa, including using OpenSSL’s libcrypto, but the easiest method is to use the encoding libraries from Google Toolbox for Mac which is conveniently distributed under the Open Source Apache License. Download GTMDefines.h, GTMBase64.h and GTMBase64.m, then add them to your project. The GTM code compiles perfectly on the iPhone and even includes web safe variants that are immediately suitable for use in URLs without percent encoding.

The Code

With the project setup complete the Paid version will respond to the custom URL launch! This will facilitate an export from the Lite version to be received by the Full version.

The Lite Version

The Lite application should be extended to add a button to trigger the export on an about, settings, or upgrade page.

The controller action code will read the data file, encode it and then format a URL with the protocol prefix / URL scheme configured in the previous step.

#import "GTMBase64.h"
...
NSData *fileData = [NSData dataWithContentsOfFile:@"/path/to/LiteApplication/Documents/file"];
NSString *encodedString = [GTMBase64 stringByWebSafeEncodingData:fileData padded:YES];
NSString *urlString = [NSString stringWithFormat:@"myapp://localhost/importDatabase?%@", encodedString];
NSURL *openURL = [NSURL URLWithString:urlString];
[[UIApplication sharedApplication] openURL:openURL];

The Full Version

The final step is to make your application handle the Open URL event and reverse the encoding process.

Add a handleOpenURL method to the AppDelegate implementation as follows. This will receive the URL, sanity check it, and then parse the query string directly. The resulting binary data is written to the file in the full application’s data directory.

#import "GTMBase64.h"
...
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    if([@"/importDatabase" isEqual:[url path]]) {
        NSString *query = [url query];
        NSString *importUrlData = [GTMBase64 webSafeDecodeString:query];

        // NOTE: In practice you will want to prompt the user to confirm before you overwrite their files!
        [importUrlData writeToFile:@"/path/to/FullApplication/Documents/file" atomically:YES];
        return YES;
    }
    return NO;
}

End Game

We have transferred files of up to 100k this way, and the size of a URL string is theoretically only limited by available memory. That said, this approach is most suitable for applications with small to medium size data transfer requirements. Best of all though, it can be easily used for SQLite databases, XML files, text files, or even extended to handled compressed archives.

Strip has seen steady growth since it’s release as people upgrade from Strip Lite to the full version. Based on download reports and direct comments from customers we believe that much of this is directly attributable to the simple upgrade path. Until (and if at all) Apple begins to allow in-app purchases from within free applications this is a simple and painless way to migrate data between Lite and Paid application versions.

  • http://matthijslangenberg.nl/ Matthijs Langenberg

    Is the /tmp directory shared system-wide? If so, can’t it be used for storing migration data?

  • Pingback: Tutorial: Migrating Data at Under The Bridge

  • http://alex@alexcurylo.com Alex Curylo

    Here’s a nifty trick for shared files some bright spark came up with: write to the phone’s DCIM folder! Makes sense once you think about it, yes?

    http://www.alexcurylo.com/blog/2009/06/05/snippet-data-sharing/

  • http://www.zetetic.net Stephen Lombardo

    @Matthijs – That’s a great thought. Using /tmp will work for applications when run under the simulator (it uses your system’s temp directory). However, /tmp isn’t available on the device itself, so you wind up in an odd situation where your code will run in one environment but not the other. There was also some perceived risk on our part that Apple might reject our application for writing data outside of the application directory. We chose the URL handler approach because it works with an identical code path on both the simulator and device, uses popular and “blessed” APIs, and as a bonus automatically launches the Paid application to save the customer a few steps.

    @Alex – That is a really awesome idea for dealing with larger files! I just have a few questions. How you compensate for the fact that the DCIM directory doesn’t have the same path in the simulator as on the device? I can’t seem to find any public API hooks to discover the actual location of the media directory from the UIImagePickerController at run time. Also, I’m somewhat concerned that reading & writing in Apple’s camera directory might be grounds for rejection. Have you submitted an application to the store that was approved by Apple using that approach?

  • Ashley Clark

    Why not put the information on the pasteboard as an NSData object using a custom type? On the launch of your new program examine the pasteboard and automatically load the data if its’ available and/or needed, during first launch only maybe?

  • Ashley Clark

    In particular [UIPasteboard pasteboardWithName:create:] and setting persistent=YES on the resulting pasteboard would even keep it from interfering with regular copy and paste.

    The new application can then issue a -removePasteboardWithName: once it’s done migrating the data over and free up whatever space the pasteboard uses during its’ lifetime.

  • http://www.zetetic.net Stephen Lombardo

    @Ashley The URL handler solution described here predates the 3.0 SDK and is intended to work with older versions of the OS as well. As you mentioned, a UIPasteboard is an alternative for 3.0-only applications. You can even leave the custom URL handler in place, sans encoded query string, to launch the Paid application and trigger the appropriate import event. Just call setData:forPasteboardType: to stash the file data during export and then dataForPasteboardType: during import.

  • http://flashback.manomio.com Stuart Carnie

    This is exactly what we did a few months back for Flashback LITE to Flashback versions. We actually used a lightweight ZIP library too, so we could send more data, by compressing and then Base64 encoding.

    One thing to be aware of is that you are limited to a URL of just under 1MB (which is still quite large, but it’s not endless).

  • Pingback: links for 2009-07-15 | manicwave.com

  • Antti

    Nice technique! I had some strange problems with it though.

    I couldn’t register the url scheme successfully on the simulator with my old project until I quit and restarted Xcode. Another strange thing was that the scheme didn’t register on the device with debug build. Release build solved the problem. Hope these don’t cause random headaches on the customer side.. :)

  • Toto

    has seen steady growth since it’s release
    –> has seen steady growth since *its* release

  • Pingback: links for 2009-11-16 « Blarney Fellow

  • http://www.fatihyasar.com Fatih YASAR

    It was very helped, thank you.

  • http://www.fatihyasar.com Fatih YASAR

    I need to ask you something about the store user data.
    I’m sotring users authentication, session etc. data in UserDefaults object, as we know this object store any data in the itself. Shall i encode this data before store them ?