Close Menu
geekfence.comgeekfence.com
    What's Hot

    Buying a phone in 2026? Follow this one rule

    February 10, 2026

    3 Questions: Using AI to help Olympic skaters land a quint | MIT News

    February 10, 2026

    Introducing the new Databricks Partner Program and Well-Architected Framework for ISVs and Data Providers

    February 10, 2026
    Facebook X (Twitter) Instagram
    • About Us
    • Contact Us
    Facebook Instagram
    geekfence.comgeekfence.com
    • Home
    • UK Tech News
    • AI
    • Big Data
    • Cyber Security
      • Cloud Computing
      • iOS Development
    • IoT
    • Mobile
    • Software
      • Software Development
      • Software Engineering
    • Technology
      • Green Technology
      • Nanotechnology
    • Telecom
    geekfence.comgeekfence.com
    Home»iOS Development»Migrating an iOS app from Paid up Front to Freemium – Donny Wals
    iOS Development

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

    AdminBy AdminJanuary 31, 2026No Comments8 Mins Read2 Views
    Facebook Twitter Pinterest LinkedIn Telegram Tumblr Email
    Migrating an iOS app from Paid up Front to Freemium – Donny Wals
    Share
    Facebook Twitter LinkedIn Pinterest Email


    Published on: January 30, 2026

    Paid up front apps can be a tough sell on the App Store. You might be getting plenty of views on your product page, but if those views aren’t converting to downloads, something has to change. That’s exactly where I found myself with Maxine: decent traffic, almost no sales.

    So I made the switch to freemium, even though I didn’t really want to. In the end, the data was pretty obvious and I’ve been getting feedback from other devs too. Free downloads with optional in-app purchases convert better and get more users through the door. After thinking about the best way to make the switch, I decided that existing users get lifetime access for free, and new users get 5 workouts before they need to subscribe or unlock a lifetime subscription. That should give them plenty of time to properly try and test the app before they commit to buying.

    In this post, we’ll explore the following topics:

    • How to grandfather in existing users using StoreKit receipt data
    • Testing gotchas you’ll run into and how to work around them
    • The release sequence that ensures a smooth transition

    By the end, you’ll know how to migrate your own paid app to freemium without leaving your loyal early adopters behind.

    Grandfathering in users through StoreKit

    Regardless of how you implement in-app purchases, you can use StoreKit to check when a user first installed your app. This lets you identify users who paid for the app before it went free and automatically grant them lifetime access.

    You can do this using the AppTransaction API in StoreKit. It gives you access to the original app version and original purchase date for the current device. It’s a pretty good way to detect users that have bought your app pre-freemium.

    Here’s how to check the first installed version (which is what I did for Maxine):

    import StoreKit
    
    func isLegacyPaidUser() async -> Bool {
      do {
        let appTransaction = try await AppTransaction.shared
    
        switch appTransaction {
        case .verified(let transaction):
          // The version string from the first install
          let originalVersion = transaction.originalAppVersion
    
          // Compare against your last paid version
          // For example, if version 2.0 was your first free release
          if let version = Double(originalVersion), version < 2.0 {
            return true
          }
          return false
    
        case .unverified:
          // Transaction couldn't be verified, treat as new user
          return false
        }
      } catch {
        // No transaction available
        return false
      }
    }

    Since this logic could potentially cause you missing out on revenue, I highly recommend writing a couple of unit tests to ensure your legacy checks work as intended. My approach to testing the legacy check involved having a method that would take the version string from AppTransaction and check it against my target version. That way I know that my test is solid. I also made sure to have tests like making sure that users that were marked pro due to version numbering were able to pass all checks done in my ProAccess helper. For example, by checking that they’re allowed to start a new workout.

    If you want to learn more about Swift Testing, I have a couple of posts in the Testing category to help you get started.

    I opted to go for version checking, but you could also use the original purchase date if that fits your situation better:

    import StoreKit
    
    func isLegacyPaidUser(cutoffDate: Date) async -> Bool {
      do {
        let appTransaction = try await AppTransaction.shared
    
        switch appTransaction {
        case .verified(let transaction):
          // When the user first installed (purchased) the app
          let originalPurchaseDate = transaction.originalPurchaseDate
    
          // If they installed before your freemium launch date, they're legacy
          return originalPurchaseDate < cutoffDate
    
        case .unverified:
          return false
        }
      } catch {
        return false
      }
    }
    
    // Usage: check if installed before your freemium release
    let isLegacy = await isLegacyPaidUser(
      cutoffDate: DateComponents(
        calendar: .current,
        year: 2026,
        month: 1,
        day: 30
      ).date!
    )

    Again, if you decide to ship a solution like this I highly recommend that you add some unit tests to avoid mistakes that could cost you revenue.

    The version approach works well when you have clear version boundaries. The date approach is useful if you’re not sure which version number will ship or if you want more flexibility.

    Once you’ve determined the user’s status, you’ll want to persist it locally so you don’t have to check the receipt every time:

    import StoreKit
    
    actor EntitlementManager {
      static let shared = EntitlementManager()
    
      private let defaults = UserDefaults.standard
      private let legacyUserKey = "isLegacyProUser"
    
      var hasLifetimeAccess: Bool {
        defaults.bool(forKey: legacyUserKey)
      }
    
      func checkAndCacheLegacyStatus() async {
        // Only check if we haven't already determined status
        guard !defaults.bool(forKey: legacyUserKey) else { return }
    
        let isLegacy = await isLegacyPaidUser()
        if isLegacy {
          defaults.set(true, forKey: legacyUserKey)
        }
      }
    
      private func isLegacyPaidUser() async -> Bool {
        do {
          let appTransaction = try await AppTransaction.shared
    
          switch appTransaction {
          case .verified(let transaction):
            if let version = Double(transaction.originalAppVersion), version < 2.0 {
              return true
            }
            return false
          case .unverified:
            return false
          }
        } catch {
          return false
        }
      }
    }

    My app is a single-device app, so I don’t have multi-device scenarios to worry about. If your app syncs data across devices, you might want a more involved solution. For example, you could store a “legacy pro” marker in CloudKit or on your server so the entitlement follows the user’s iCloud account rather than being tied to a single device.

    Also, storing in UserDefaults is a somewhat naive approach. Depending on your minimum OS version, you might run your app in a potentially jailbroken environment; this would allow users to tamper with UserDefaults quite easily and it would be much more secure to store this information in the keychain, or to check your receipt every time instead. For simplicity I’m using UserDefaults in this post, but I recommend you make a proper security risk assessment on which approach works for you.

    With this code in place, you’re all set up to start testing…

    Testing gotchas

    Testing receipt-based grandfathering has some quirks you should know about before you ship.

    TestFlight always reports version 1.0

    When your app runs via TestFlight it runs in a sandboxed environment and AppTransaction.originalAppVersion returns "1.0" regardless of which build the tester actually installed. This makes it impossible to test version-based logic through TestFlight alone.

    You can get around this using debug builds with a manual toggle that lets you simulate being a legacy user. Add a hidden debug menu or use launch arguments to override the legacy check during development.

    #if DEBUG
    var debugOverrideLegacyUser: Bool? = nil
    #endif
    
    func isLegacyPaidUser() async -> Bool {
      #if DEBUG
      if let override = debugOverrideLegacyUser {
        return override
      }
      #endif
    
      // Normal receipt-based check...
    }

    Reinstalls reset the original version.

    If a user deletes and reinstalls your app, the originalAppVersion reflects the version they reinstalled, not their very first install. This is a limitation of on-device receipt data. If you’ve written the user’s pro-status to the keychain, you would actually be able to pull the pro status from there.

    Sadly I haven’t found a fail-proof way to get around reinstalls and receipts resetting. For my app, this is acceptable. I don’t have that many users so I think we’ll be okay in terms of risk of someone losing their legacy pro access.

    Device clock manipulation.

    Users with incorrect device clocks could work their way around your date-based checks. That’s why I went with version-based checking but again, it’s all a matter of determining what an acceptable risk is for you and your app.

    Making the move

    When you’re ready to release, the sequence matters. Here’s what I did:

    1. Set your app to manual release. In App Store Connect, configure your new version for manual release rather than automatic. This gives you control over timing.

    2. Add a note for App Review. In the reviewer notes, explain that you’ll switch the app’s price to free before releasing. Something like: “This update transitions the app from paid to freemium. I will change the price to free in App Store Connect before releasing this version to ensure a smooth transition for users.”

    3. Wait for approval. Let App Review approve your build while it’s still technically a paid app.

    4. Make the app free first. Once approved, go to App Store Connect and change your app’s price to free (or set up your freemium pricing tiers).

    5. Then release. After the price change is live, manually release your approved build.

    I’m not 100% sure the order matters, but making the app free before releasing felt like the safest approach. It ensures that the moment users can download your new freemium version, they’re not accidentally charged for the old paid model.

    In Summary

    Grandfathering paid users when switching to freemium comes down to checking AppTransaction for the original install version or date. Cache the result locally, and consider CloudKit or server-side storage if you need cross-device entitlements.

    Testing is tricky because TestFlight always reports version 1.0 and sandbox receipts don’t perfectly mirror production. Use debug toggles and, ideally, a real device with an older App Store build for thorough testing.

    When you release, set your build to manual release, add a note for App Review explaining the transition, then make the app free before you tap the release button.

    Changing your monetization strategy can feel like admitting defeat, but it’s really just iteration. The App Store is competitive, user expectations shift, and what worked at launch might not work six months later. Pay attention to your conversion data, be willing to adapt, and don’t let sunk-cost thinking keep you stuck with a model that isn’t serving your users or your business.



    Source link

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email

    Related Posts

    Swift command design pattern – The.Swift.Dev.

    February 10, 2026

    SwiftUI TabView (.page / PageTabViewStyle) selection can get out of sync when user interrupts a programmatic page change

    February 9, 2026

    An Introduction to Liquid Glass for iOS 26

    February 7, 2026

    DTCoreText 1.6.27 | Cocoanetics

    February 5, 2026

    UICollectionView data source and delegates programmatically

    February 4, 2026

    ZStack background ignoring safe area while content respects it, all scrolling together in ScrollView

    February 3, 2026
    Top Posts

    Hard-braking events as indicators of road segment crash risk

    January 14, 202617 Views

    Understanding U-Net Architecture in Deep Learning

    November 25, 202512 Views

    Achieving superior intent extraction through decomposition

    January 25, 20268 Views
    Don't Miss

    Buying a phone in 2026? Follow this one rule

    February 10, 2026

    Summary created by Smart Answers AIIn summary:Tech Advisor advises following the ‘previous generation rule’ when…

    3 Questions: Using AI to help Olympic skaters land a quint | MIT News

    February 10, 2026

    Introducing the new Databricks Partner Program and Well-Architected Framework for ISVs and Data Providers

    February 10, 2026

    Threat Observability Updates in Secure Firewall 10.0

    February 10, 2026
    Stay In Touch
    • Facebook
    • Instagram
    About Us

    At GeekFence, we are a team of tech-enthusiasts, industry watchers and content creators who believe that technology isn’t just about gadgets—it’s about how innovation transforms our lives, work and society. We’ve come together to build a place where readers, thinkers and industry insiders can converge to explore what’s next in tech.

    Our Picks

    Buying a phone in 2026? Follow this one rule

    February 10, 2026

    3 Questions: Using AI to help Olympic skaters land a quint | MIT News

    February 10, 2026

    Subscribe to Updates

    Please enable JavaScript in your browser to complete this form.
    Loading
    • About Us
    • Contact Us
    • Disclaimer
    • Privacy Policy
    • Terms and Conditions
    © 2026 Geekfence.All Rigt Reserved.

    Type above and press Enter to search. Press Esc to cancel.