Switching to Freemium

Lessons Learned From
Changing iOctocat's Pricing Model

CocoaHeads Bremen   -   December 16th 2013
Dennis Reimann   -   @dennisreimann

Technical aspects

App Store Receipt

  • Updated with iOS 7
  • Unified receipt format, same as on OS X
  • Transition paid to free with in-app purchase
  • Supports various demo scenarios
The App Store receipt

Details in WWDC 2013, Session 308

RMStore

  • Lightweight wrapper for StoreKit
  • Blocks and Notifications instead of Delegation
  • Receipt verification
  • Purchase management
  • Good documentation and full test coverage

Store setup


// IOCAppDelegate.m

- (void)setupStore {
    self.receiptVerificator = [[RMStoreAppReceiptVerificator alloc] init];
    self.receiptVerificator.bundleIdentifier = IOCStoreBundleIdentifier;
    self.receiptVerificator.bundleVersion = IOCStoreBundleVersion;

    [RMStore defaultStore].receiptVerificator = self.receiptVerificator;

    [self setNeedsProVersionCheck];
}

- (void)setNeedsProVersionCheck {
    self.proVersionStatus = IOCProVersionStatusUndef;
}
					

Receipt refresh


// IOCAppDelegate.m

- (void)verifyReceipt {
    if (![self.receiptVerificator verifyAppReceipt]) {
        [[RMStore defaultStore] refreshReceiptOnSuccess:^{
            [self setNeedsProVersionCheck];
        } failure:NULL];
    }
}
					

Check on launch


// IOCAppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // ...

    [self setupStore];
    [self verifyReceipt];

    // ...
}
					

Pro version check


// IOCAppDelegate.m

- (BOOL)isProVersion {
    if (self.proVersionStatus == IOCProVersionStatusUndef) {
        RMAppReceipt *r = [RMAppReceipt bundleReceipt];

        // see if it has been bought prior to the freemium switch,
        // otherwise check for the pro upgrade purchase
        BOOL boughtBeforeSwitch =
            [IOCUtil versionBoughtBeforeSwitch:r.originalAppVersion];
        BOOL boughtProUpgrade =
            [r containsInAppPurchaseOfProductIdentifier:IOCProIdentifier];
        self.proVersionStatus = boughtBeforeSwitch || boughtProUpgrade ?
            IOCProVersionStatusTrue : IOCProVersionStatusFalse;
    }
    return self.proVersionStatus;
}
					
Root view
Upgrade view

Upgrading


// IOCUpgradeController.m

- (IBAction)purchaseUpgrade:(id)sender {
    [[RMStore defaultStore] addPayment:IOCStoreProUpgradeIdentifier success:^(SKPaymentTransaction *transaction) {
        [[iOctocat sharedInstance] setNeedsProVersionCheck];

        [self handleUpgradeSuccess];
    } failure:^(SKPaymentTransaction *transaction, NSError *error) {
        [self alertError:error.localizedDescription];
    }];
}
					

Restoring purchases


// IOCUpgradeController.m

- (IBAction)restorePurchase:(id)sender {
    [[RMStore defaultStore] restoreTransactionsOnSuccess:^{
        [[iOctocat sharedInstance] setNeedsProVersionCheck];

        if ([iOctocat sharedInstance].isProVersion) {
            [self handleUpgradeSuccess];
        } else {
            [self alertError:NSLocalizedString(@"You have not purchased iOctocat Pro with the given Apple ID, yet.", nil)];
        }
    } failure:^(NSError *error) {
        [self alertError:error.localizedDescription];
    }];
}
					

Upgrade handling


// Any ViewController with Upgrade references

- (void)viewDidLoad {
    [super viewDidLoad];

    [[RMStore defaultStore] addStoreObserver:self];

    [self updateViewDependingOnProVersionStatusAnimated:NO];
}

- (void)updateViewDependingOnProVersionStatusAnimated:(BOOL)animated {
    UIBarButtonItem *item = nil;
    if (![IOCAppDelegate sharedInstance].isProVersion) {
        item = [[UIBarButtonItem alloc] initWithTitle:@"Upgrade" ...];
    }
    [self.navigationItem setRightBarButtonItem:item animated:animated];
}
					

Notification handling


// Any ViewController with Upgrade references

- (void)storeRefreshReceiptFinished:(NSNotification*)notification {
    [self updateViewDependingOnProVersionStatusAnimated:YES];
}

- (void)storePaymentTransactionFinished:(NSNotification*)notification {
    [self updateViewDependingOnProVersionStatusAnimated:YES];
}

- (void)storeRestoreTransactionsFinished:(NSNotification *)notification {
    [self updateViewDependingOnProVersionStatusAnimated:YES];
}
					

Business aspects

Reasons

  • Lack of demo versions
  • Stagnating sales
  • Potentially bigger market
  • Kick it like GitHub

Before

Sales/downloads before the switch

266 paid sales/downloads         1397 EUR revenue

After

Sales after the switch

217 paid sales         1110 EUR revenue

After

Downloads after the switch

6261 downloads

Low conversion rate

Right now missing out on potential customers

@dennisreimann must be crazy to give it away for free

Next steps

  • Improving conversions
  • Make upgrading more desirable for heavy users
  • Maybe cut off some features worth paying for
  • Try to upsell more efficiently
Root view

Thank you!