20.1 C
Canberra
Tuesday, March 3, 2026

Migrating an iOS app from Paid up Entrance to Freemium – Donny Wals


Revealed on: January 30, 2026

Paid up entrance apps is usually a robust promote on the App Retailer. You is likely to be getting loads of views in your product web page, but when these views aren’t changing to downloads, one thing has to vary. That is precisely the place I discovered myself with Maxine: respectable site visitors, virtually no gross sales.

So I made the change to freemium, though I did not actually wish to. In the long run, the info was fairly apparent and I have been getting suggestions from different devs too. Free downloads with optionally available in-app purchases convert higher and get extra customers via the door. After eager about one of the best ways to make the change, I made a decision that current customers get lifetime entry at no cost, and new customers get 5 exercises earlier than they should subscribe or unlock a lifetime subscription. That ought to give them loads of time to correctly attempt to check the app earlier than they commit to purchasing.

On this submit, we’ll discover the next matters:

  • Learn how to grandfather in current customers utilizing StoreKit receipt information
  • Testing gotchas you may run into and how one can work round them
  • The discharge sequence that ensures a easy transition

By the tip, you may know how one can migrate your individual paid app to freemium with out leaving your loyal early adopters behind.

Grandfathering in customers via StoreKit

No matter the way you implement in-app purchases, you should utilize StoreKit to test when a person first put in your app. This allows you to determine customers who paid for the app earlier than it went free and robotically grant them lifetime entry.

You are able to do this utilizing the AppTransaction API in StoreKit. It provides you entry to the unique app model and authentic buy date for the present system. It is a fairly good technique to detect customers which have purchased your app pre-freemium.

This is how one can test the primary put in model (which is what I did for Maxine):

import StoreKit

func isLegacyPaidUser() async -> Bool {
  do {
    let appTransaction = strive await AppTransaction.shared

    change appTransaction {
    case .verified(let transaction):
      // The model string from the primary set up
      let originalVersion = transaction.originalAppVersion

      // Evaluate towards your final paid model
      // For instance, if model 2.0 was your first free launch
      if let model = Double(originalVersion), model < 2.0 {
        return true
      }
      return false

    case .unverified:
      // Transaction could not be verified, deal with as new person
      return false
    }
  } catch {
    // No transaction accessible
    return false
  }
}

Since this logic might doubtlessly trigger you lacking out on income, I extremely suggest writing a few unit assessments to make sure your legacy checks work as meant. My method to testing the legacy test concerned having a way that will take the model string from AppTransaction and test it towards my goal model. That method I do know that my check is strong. I additionally made positive to have assessments like ensuring that customers that have been marked professional attributable to model numbering have been in a position to move all checks completed in my ProAccess helper. For instance, by checking that they are allowed to begin a brand new exercise.

If you wish to be taught extra about Swift Testing, I’ve a few posts within the Testing class that will help you get began.

I opted to go for model checking, however you can additionally use the unique buy date if that matches your state of affairs higher:

import StoreKit

func isLegacyPaidUser(cutoffDate: Date) async -> Bool {
  do {
    let appTransaction = strive await AppTransaction.shared

    change appTransaction {
    case .verified(let transaction):
      // When the person first put in (bought) the app
      let originalPurchaseDate = transaction.originalPurchaseDate

      // In the event that they put in earlier than your freemium launch date, they're legacy
      return originalPurchaseDate < cutoffDate

    case .unverified:
      return false
    }
  } catch {
    return false
  }
}

// Utilization: test if put in earlier than your freemium launch
let isLegacy = await isLegacyPaidUser(
  cutoffDate: DateComponents(
    calendar: .present,
    yr: 2026,
    month: 1,
    day: 30
  ).date!
)

Once more, should you determine to ship an answer like this I extremely suggest that you just add some unit assessments to keep away from errors that might price you income.

The model method works nicely when you may have clear model boundaries. The date method is beneficial should you’re undecided which model quantity will ship or in order for you extra flexibility.

As soon as you have decided the person’s standing, you may wish to persist it domestically so you do not have to test the receipt each time:

import StoreKit

actor EntitlementManager {
  static let shared = EntitlementManager()

  non-public let defaults = UserDefaults.normal
  non-public let legacyUserKey = "isLegacyProUser"

  var hasLifetimeAccess: Bool {
    defaults.bool(forKey: legacyUserKey)
  }

  func checkAndCacheLegacyStatus() async {
    // Solely test if we have not already decided standing
    guard !defaults.bool(forKey: legacyUserKey) else { return }

    let isLegacy = await isLegacyPaidUser()
    if isLegacy {
      defaults.set(true, forKey: legacyUserKey)
    }
  }

  non-public func isLegacyPaidUser() async -> Bool {
    do {
      let appTransaction = strive await AppTransaction.shared

      change appTransaction {
      case .verified(let transaction):
        if let model = Double(transaction.originalAppVersion), model < 2.0 {
          return true
        }
        return false
      case .unverified:
        return false
      }
    } catch {
      return false
    }
  }
}

My app is a single-device app, so I haven’t got multi-device eventualities to fret about. In case your app syncs information throughout gadgets, you may want a extra concerned answer. For instance, you can retailer a “legacy professional” marker in CloudKit or in your server so the entitlement follows the person’s iCloud account fairly than being tied to a single system.

Additionally, storing in UserDefaults is a considerably naive method. Relying in your minimal OS model, you may run your app in a doubtlessly jailbroken setting; this may enable customers to tamper with UserDefaults fairly simply and it might be rather more safe to retailer this info within the keychain, or to test your receipt each time as an alternative. For simplicity I am utilizing UserDefaults on this submit, however I like to recommend you make a correct safety threat evaluation on which method works for you.

With this code in place, you are all set as much as begin testing…

Testing gotchas

Testing receipt-based grandfathering has some quirks you must find out about earlier than you ship.

TestFlight at all times stories model 1.0

When your app runs by way of TestFlight it runs in a sandboxed setting and AppTransaction.originalAppVersion returns "1.0" no matter which construct the tester truly put in. This makes it not possible to check version-based logic via TestFlight alone.

You will get round this utilizing debug builds with a handbook toggle that permits you to simulate being a legacy person. Add a hidden debug menu or use launch arguments to override the legacy test throughout growth.

#if DEBUG
var debugOverrideLegacyUser: Bool? = nil
#endif

func isLegacyPaidUser() async -> Bool {
  #if DEBUG
  if let override = debugOverrideLegacyUser {
    return override
  }
  #endif

  // Regular receipt-based test...
}

Reinstalls reset the unique model.

If a person deletes and reinstalls your app, the originalAppVersion displays the model they reinstalled, not their very first set up. This can be a limitation of on-device receipt information. In case you’ve written the person’s pro-status to the keychain, you’d truly have the ability to pull the professional standing from there.

Sadly I have not discovered a fail-proof technique to get round reinstalls and receipts resetting. For my app, that is acceptable. I haven’t got that many customers so I feel we’ll be okay by way of threat of somebody dropping their legacy professional entry.

Machine clock manipulation.

Customers with incorrect system clocks might work their method round your date-based checks. That is why I went with version-based checking however once more, it is all a matter of figuring out what an appropriate threat is for you and your app.

Making the transfer

While you’re able to launch, the sequence issues. This is what I did:

  1. Set your app to handbook launch. In App Retailer Join, configure your new model for handbook launch fairly than automated. This provides you management over timing.

  2. Add a be aware for App Evaluate. Within the reviewer notes, clarify that you will change the app’s value to free earlier than releasing. One thing like: “This replace transitions the app from paid to freemium. I’ll change the value to free in App Retailer Join earlier than releasing this model to make sure a easy transition for customers.”

  3. Look forward to approval. Let App Evaluate approve your construct whereas it is nonetheless technically a paid app.

  4. Make the app free first. As soon as accepted, go to App Retailer Join and alter your app’s value to free (or arrange your freemium pricing tiers).

  5. Then launch. After the value change is dwell, manually launch your accepted construct.

I am not 100% positive the order issues, however making the app free earlier than releasing felt just like the most secure method. It ensures that the second customers can obtain your new freemium model, they don’t seem to be by chance charged for the outdated paid mannequin.

In Abstract

Grandfathering paid customers when switching to freemium comes all the way down to checking AppTransaction for the unique set up model or date. Cache the consequence domestically, and think about CloudKit or server-side storage should you want cross-device entitlements.

Testing is difficult as a result of TestFlight at all times stories model 1.0 and sandbox receipts do not completely mirror manufacturing. Use debug toggles and, ideally, an actual system with an older App Retailer construct for thorough testing.

While you launch, set your construct to handbook launch, add a be aware for App Evaluate explaining the transition, then make the app free earlier than you faucet the discharge button.

Altering your monetization technique can really feel like admitting defeat, but it surely’s actually simply iteration. The App Retailer is aggressive, person expectations shift, and what labored at launch won’t work six months later. Take note of your conversion information, be keen to adapt, and do not let sunk-cost considering hold you caught with a mannequin that is not serving your customers or your enterprise.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

[td_block_social_counter facebook="tagdiv" twitter="tagdivofficial" youtube="tagdiv" style="style8 td-social-boxed td-social-font-icons" tdc_css="eyJhbGwiOnsibWFyZ2luLWJvdHRvbSI6IjM4IiwiZGlzcGxheSI6IiJ9LCJwb3J0cmFpdCI6eyJtYXJnaW4tYm90dG9tIjoiMzAiLCJkaXNwbGF5IjoiIn0sInBvcnRyYWl0X21heF93aWR0aCI6MTAxOCwicG9ydHJhaXRfbWluX3dpZHRoIjo3Njh9" custom_title="Stay Connected" block_template_id="td_block_template_8" f_header_font_family="712" f_header_font_transform="uppercase" f_header_font_weight="500" f_header_font_size="17" border_color="#dd3333"]
- Advertisement -spot_img

Latest Articles