Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by ZenithGuardian471

Why Does My StoreKit 2 Business Model Change Work in Testing But Not in Production?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I'm updating my business model according to Apple’s guidelines and implemented a task in the main view of my app. The feature works perfectly on the simulator, physical devices, and TestFlight. However, after releasing the new version to production, all users (both new and existing) are asked to subscribe, even though the console shows that transactions are correctly retrieved. The challenge is that I’m not sure how to view the console logs in production to debug what’s being retrieved.

Here is the sandbox receipt I obtained

JSON
AppTransaction.shared obtained: { "applicationVersion" : "1", "bundleId" : ***** "deviceVerification" : "6M0Nnw14nSEOBVTPE//EfnWSwLm7LFSlrpFEwxgH74SBHp5dSzBEm896Uvo42mwr", "deviceVerificationNonce" : "8a8238c0-0aee-41e6-bfb0-1cfc52b70fb6", "originalApplicationVersion" : "1.0", "originalPurchaseDate" : 1375340400000, "receiptCreationDate" : 1737577840917, "receiptType" : "Sandbox", "requestDate" : 1737577840917 }

This is the processing log while verifying the receipt

BASH
New business model change: 1.7 Original versionéis components: ["1", "0"] Major version: 1, Minor version: 0 This user is premium. Original version: 1.0

This is my task implementation

SWIFT
.task { do { let shared = try await AppTransaction.shared if case .verified(let appTransaction) = shared { let newBusinessModelVersion = (1, 7) // Represented as (major, minor) let versionComponents = appTransaction.originalAppVersion.split(separator: ".") if let majorVersion = versionComponents.first.flatMap({ Int($0) }), let minorVersion = versionComponents.dropFirst().first.flatMap({ Int($0) }) { if (majorVersion, minorVersion) < newBusinessModelVersion { self.premiumStatus.isPremium = true isPremium = true } else { let customerInfo = try await Purchases.shared.customerInfo() self.premiumStatus.isPremium = customerInfo.entitlements["premium"]?.isActive == true isPremium = self.premiumStatus.isPremium } } else { print("Error: obteining version components") } } else { print("Not verified") } } catch { print("Error processing transaction: \(error.localizedDescription)") } }

I’m looking for suggestions on how to capture production logs to better debug what’s happening with receipt retrieval and processing, and any insight as to why the behavior differs from testing environments.

0

Comments 0

Answers 3

Views 64

3 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

Below are a few tips based on Apple’s guidelines and our documentation for troubleshooting your issue:

1. Viewing Production Logs

In production you won’t see a live “console” like you do during development. To view logs from a production device you can:

  • Use Xcode’s Devices and Simulators window: Connect a device to your Mac, open Xcode, choose Window → Devices and Simulators, then select your device. You can see the device’s console logs there.
  • Use the Console app on macOS: With your device connected, open the Console app to view the live logs.
  • Integrate remote logging: For more persistent logging, consider using Apple’s unified logging system (via OSLog) and/or a remote logging framework. This way you can capture logs sent from users’ devices after release.

2. Receipt Details in Production

Make sure that your receipt contains the expected information. Note the following:

  • Original Application Version: In production, check that the originalApplicationVersion field in the receipt is being set correctly. For users who have never made a purchase in your app, the receipt might have different characteristics versus a development or TestFlight receipt.
  • Transaction Visibility: The logic you use with AppTransaction.shared confirms the receipt is obtained and “verified” on your device, but production receipts sometimes may differ. For instance, users who haven’t made a purchase may cause the receipt to be absent or incomplete affecting your version check.

3. Considerations for Changing the Business Model

Your code sets premium access if the original app version is less than the new business model version. In production:

  • Verify that the receipt’s originalApplicationVersion value is what you expect. If the users’ receipts (even from users who purchased previously) do not have the correct version format (e.g., “1.0”), then your version comparison might not work in production.
  • Confirm that you’re not inadvertently regenerating receipts (or that receipts aren’t missing) once the new version is deployed. If no valid receipt is found, your fallback behavior might be requiring a subscription.

Next Steps

  1. Connect a Production Device: Use Xcode’s Devices and Simulators or Console.app to capture live logs while reproducing the issue on a production-installed app.

  2. Add More Detailed Logging: For example, log the entire decoded receipt information (using OSLog without exposing sensitive data), so you can compare what’s actually received in production versus what you see in sandbox/TestFlight.

  3. Review Receipt Validation Flow: Ensure your receipt validation on the device and your server (if any) checks that align with Apple’s guidelines. See Validating receipts on the device for details.

By using these techniques you should be able to capture and review the production logs and better understand why your business model change isn’t recognized as expected.

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by NovaExplorer535 1 month ago

0

This seems to be exactly what’s happening, Apple does not consider the original version as the commercial version. I can imagine that it takes the build uploaded to TestFlight (not even the one shown in Xcode)

enter image description here

In the Xcode console, when retrieving the current version, it prints the build number.

enter image description here

The issue with this is that for each version of the app, I have uploaded 3-4 or even 10 builds, and all of these (users who paid for the app) must be able to download it as premium. Because of this, I assume that there are users with an original version between 1 and 10, so this value is not useful for me.

enter image description here

Don't you think OriginalPurchaseDate could be a solution?

SWIFT
func fetchActiveTransactions() async { let cohortDateComponents = DateComponents(year: 2025, month: 1, day: 24, hour: 0, minute: 0, second: 0) let cohortDate = Calendar.current.date(from: cohortDateComponents)!) let shared = try? await AppTransaction.shared print(shared as Any) if case .verified(let appTransaction) = shared { print("Verified \(appTransaction)") let originalPurchaseDate = appTransaction.originalPurchaseDate print("Original Purchase Date: \(originalPurchaseDate)") if originalPurchaseDate < cohortDate { print("\(originalPurchaseDate) is before to \(cohortDate). Is premium.") await MainActor.run { print("User is premium because \(originalPurchaseDate) is before to \(cohortDate).") self.isPremium = true self.isLegacyPremium = true self.purchaseDate = appTransaction.originalPurchaseDate } return } else { print("\(originalPurchaseDate) is after \(cohortDate). You need to validate it in RevenueCat.") do { print("Checking premium status in RevenueCat...") let customerInfo = try await Purchases.shared.customerInfo() self.isPremium = customerInfo.entitlements["premium"]?.isActive == true print("Premium State: \(self.isPremium)") } catch { print("Error obtaining customer info from RevenueCat: \(error.localizedDescription)") } } } else { print("Cannot fetch transactions.") } }

No comments yet.

Answer by NeptunianTraveler643 1 month ago

0

It sounds like the issue might be related to how the CFBundleVersion is defined in your app. If you’re using a build number (e.g., an incremental count of commits) for CFBundleVersion instead of the app’s version number (e.g., major.minor.patch), this could cause problems in production.

Apple’s documentation for the originalAppVersion states:

The originalAppVersion [...] contains the original value of the CFBundleShortVersionString for apps running on macOS, and the original value of the CFBundleVersion for apps running on all other platforms.

In the sandbox testing environment, the originalAppVersion value is always 1.0.

This behavior explains why everything works correctly in testing environments (simulator, physical devices, and TestFlight) where originalAppVersion defaults to 1.0. However, in production, the value of CFBundleVersion is used, and if it doesn’t match the expected format, it could lead to issues with determining whether a user is new or existing.

To address this, ensure that CFBundleVersion contains the app's version number (major.minor.patch) for production builds, or update your logic to account for your current use of build numbers.

No comments yet.

Discussion

No comments yet.