Skip to main content
Testing in-app purchases against Apple’s servers requires setup in App Store Connect: products, agreements, banking and tax information. Until all of it is in place, Product.products(for:) returns an empty array without an error, and your paywall renders with nothing in it. The standard way to develop against this is StoreKit Testing, Apple’s local testing mechanism: your app loads test products from a .storekit configuration file instead of from Apple’s servers, so you can build and test the entire purchase flow before anything exists in App Store Connect. Modaal supports it directly — select a .storekit file in Run Options, run, and your paywall fills in.
You don’t need to write Swift to follow this guide. Everything below is a setting in Modaal or a prompt you can give to your agent. If your app doesn’t have a paywall yet, start with Starting from scratch.

The three StoreKit environments

Your app can load products from three different sources. Most paywall problems come down to being in a different environment than you think, so it’s worth understanding how the three differ. The .storekit file is a launch setting, not part of your app. Modaal applies it when it starts the app, which is why the change takes effect immediately in the simulator — and why the file can never end up in a TestFlight or App Store build. It isn’t compiled into the binary.
Start in the StoreKit Test environment. It has no dependency on App Store Connect, so you can build and iterate on a paywall before you’ve created a product, signed an agreement, or filled in a tax form.

Select a StoreKit configuration

The cogwheel to the right of the Run button opens Run Options, Modaal’s equivalent of the scheme editor in Xcode.
Modaal toolbar with the Run Options cogwheel highlighted

The Run Options cogwheel, immediately right of the Run button.

1

Open Run Options

Click the cogwheel. Modaal lists every .storekit file it finds in your project, so there’s nothing to type or browse for.
2

Choose a configuration

Run Options dialog with a StoreKit Configuration dropdown

Choose a .storekit file — or None to use your App Store Connect products.

Modaal saves your choice in the project’s scheme, so it persists between sessions, survives rebuilds, and appears in Xcode as well.
3

Run

The setting takes effect the next time you run the app; it doesn’t affect an app that’s already running. Run the app and the paywall fills in.
The build log records the result of every run:
Build log line reading: StoreKit configuration: Onesec/Onesec.storekit → simulator

Applied — the app loads products from your local file.

Build log line reading: StoreKit configuration: none — cleared

Cleared — the app requests your real products from App Store Connect.

Edits to the .storekit file also take effect on the next run. Change a price, run the app, and the new price appears — you don’t need to delete the app or reset the simulator.

Confirm which environment you’re in

Product identifiers can’t tell you which environment you’re in. The identifiers in your .storekit file are deliberately the same as the ones in App Store Connect — that’s what lets the same code run in every environment — so a populated paywall looks equally plausible in either one. Prices and product names are the reliable signal. Here is the same app, in the same simulator, with the same product identifiers, run twice:
Paywall price row showing $0.99 per month and $7.99 per year

With the local configuration selected — the prices come from the .storekit file.

Paywall price row showing $0.99 per month and $6.99 per year

With StoreKit Configuration set to None — these are the live App Store Connect prices.

The annual plan costs 7.99inonerunand7.99 in one run and 6.99 in the other, and the discount badge recalculates to match. The app is behaving correctly in both cases; it’s loading its products from a different source.
A paywall that looks correct isn’t proof that your configuration is applied. When the configuration is cleared, the app requests products from Apple without reporting an error, and if your App Store Connect setup is complete you get real products at real prices. When in doubt, compare a price on screen with the price in your .storekit file.

The setting is shared with Xcode

Modaal writes your selection into the project’s scheme — the same place the scheme editor in Xcode stores it. Open the project in Xcode and the configuration is already selected; run it with ⌘R and you get the same result. There’s nothing to keep in sync, and no reason to avoid opening the project in Xcode when you want its additional StoreKit tooling — the Debug → StoreKit → Manage Transactions window lets you refund, expire, and fast-forward subscriptions by hand.

Testing on a real device

Modaal applies the configuration on the simulator. On a physical device it writes the setting into your scheme but can’t inject it at launch; that part of the mechanism belongs to Xcode. To use local test products on a device, open the project in Xcode and run it from there. Both the Run Options dialog and the build log note this, so you always know which case you’re in.
This is a limitation of how Modaal launches apps on devices, not a limitation of StoreKit. Local test products work on a physical iPhone in both Debug and Release builds — the app just has to be launched by Xcode.

Starting from scratch

If your app has no paywall yet, the fastest route is to build it against local products from the start:
1

Ask for the products and the paywall together

Add a Premium subscription with monthly and annual plans and a one-week free trial. Create a StoreKit configuration file so we can test it locally, then build a paywall screen that loads the plans and shows the annual savings.
2

Select the configuration in Run Options and run

Confirm that the build log says the configuration was applied, and that the prices on screen match your file.
3

Iterate on the design

Change prices, trial lengths, and copy in the .storekit file and run again. None of this touches App Store Connect, so there’s nothing to wait for and nothing to undo.
4

Create the real products

When the flow is right, create the products in App Store Connect with the same product identifiers, complete your agreements, and do a sandbox pass on a device.

Troubleshooting

Start with the build log: was a configuration applied or not? The two cases have entirely different causes.If a configuration was applied (the log shows → simulator) and the paywall is still empty, check the following, in order of likelihood:
  1. Product identifier mismatch. The identifiers in the .storekit file must match the identifiers your code requests exactly. This is the most common cause.
  2. A subscription outside its group. In a .storekit file, auto-renewable subscriptions belong in a subscription group, not in the general products list. A subscription in the wrong list is never returned.
  3. You’re on a physical device. See Testing on a real device.
A prompt for your agent:
The paywall is empty. We’re running with the StoreKit configuration applied. Compare the product IDs in the .storekit file against the IDs the app requests, and check that subscriptions are in a subscription group. Fix any mismatch.
If no configuration was applied (the log shows cleared, or there’s no StoreKit line at all), the app is requesting products from App Store Connect — continue to the checklist below.
Each of the following produces the same symptom — an empty product list with no error. Check them in order of how often each is the cause:
  1. The Paid Applications Agreement isn’t active. This is the most common cause. In App Store Connect, under Business, the agreement must be signed, the tax forms completed, and a bank account attached. Until all three are done, StoreKit returns no products, and nothing else on this list matters.
  2. The product is in the Missing Metadata state. Products in this state are never returned. Add a reference name, product identifier, price, and at least one localization — and for subscriptions, a group and a duration. A product in the Ready to Submit state is returned.
  3. The bundle identifier doesn’t match. Your app’s bundle identifier must exactly match an existing App Store Connect record. The build itself doesn’t need to be uploaded.
  4. The changes haven’t propagated yet. New products and metadata changes can take from a few minutes up to an hour to appear. If everything above is in order, wait and try again — there’s nothing to fix in your code.
App Review approval is not required for any of this. Products in the Ready to Submit, Waiting for Review, or In Review states — even rejected products — are all returned outside production.
This is almost always the environment rather than a bug. Compare the price on screen with the displayPrice in your .storekit file:
  • If they match, the app is using your local test products.
  • If they differ, the app is using your real App Store Connect products, and the configuration isn’t applied.
If you expected local products, open Run Options, confirm that a file is selected, and run again. The setting takes effect on the next run.
A completed purchase and a granted entitlement are two different things, and the gap between them is where hand-written purchase code usually goes wrong. Check for three causes:
  1. The transaction was never finished. Finish every successful transaction after you grant access. StoreKit redelivers unfinished transactions indefinitely, which can leave the user stuck.
  2. Access is stored instead of derived. A flag like isPremium = true saved at purchase time survives refunds, expirations, cancellations, and reinstalls. Derive access from the current entitlements at every launch instead.
  3. Nothing listens for transaction updates. Purchases can complete outside your paywall — an approved Ask to Buy request, a renewal, a purchase made on another device, a refund. Start a transaction listener at app launch so the app hears about them.
After a successful purchase the app still shows the free tier. Check that we finish the transaction after granting access, that premium status is derived from the current entitlements rather than a stored flag, and that we have a transaction updates listener running from launch.
To verify the fix, buy the subscription with a local configuration applied, then expire or refund it from Xcode’s Debug → StoreKit → Manage Transactions window and confirm that the app revokes access.
Restoring doesn’t replay a purchase record you keep — it re-reads the user’s current entitlements from Apple. When Restore appears to do nothing, it’s usually one of the following:
  • The button runs a purchase flow instead of re-reading entitlements.
  • Access is stored locally, so a fresh install has nothing to read and nothing to restore.
  • The purchase was made in a different environment. A purchase made against local test products doesn’t exist in sandbox, and vice versa — restore in the environment you bought in.
To test it properly, make a purchase, delete the app from the simulator, reinstall with Run, and tap Restore.
Delete and reinstall the app, then check that Restore Purchases brings back the subscription without asking me to buy it again.
Simulating failure is what the StoreKit Test environment is for, and the main reason not to do all of your testing in sandbox.With a configuration applied, open the project in Xcode and use Debug → StoreKit → Manage Transactions to fail transactions, approve or decline Ask to Buy requests, expire subscriptions, trigger billing retry, or issue refunds. The .storekit file also has settings for injecting errors and simulating slow networks.Scenarios worth covering before you ship:
  • Purchase interrupted (app killed mid-transaction)
  • Purchase declined by the payment method
  • Subscription expires and isn’t renewed
  • Subscription enters billing retry, then recovers
  • A refund is issued after the fact
  • Ask to Buy — a parent approves or denies
Write a test that covers what happens when a purchase is interrupted and the app is relaunched before the transaction finishes. The entitlement should still be granted on relaunch.
This is expected behavior, and the most important gap to close before shipping.TestFlight builds always use the sandbox environment. Your .storekit file was never part of the build, so a TestFlight build is your app’s first real contact with App Store Connect — and the first moment the agreement and metadata problems from the checklist above can surface.Sandbox differs from StoreKit Test in a few ways:
  • Testers use their regular Apple Account; no separate sandbox account is needed.
  • Purchases are free, and subscription renewals are accelerated.
  • Auto-renewable subscriptions stop renewing after about six cycles.
  • None of the StoreKit Test tools are available — no forced failures, no time travel, no clearing transactions.
Before you rely on TestFlight, do one sandbox pass on a real device: sign in under Settings → Developer → Sandbox Apple Account and run a build with no .storekit file attached. That run is what proves your receipts, your server notifications, and your backend against Apple’s real servers.
Ask your agent to create one. It knows the format and where the file belongs:
Create a StoreKit configuration file for the app with our subscription products, and wire it up so I can select it in Run Options.
The agent adds the file and configures the project so the file stays out of your shipped app — local test prices don’t belong in a build that goes to the App Store.You can also create one in Xcode with File → New → File → StoreKit Configuration File. Either way, the file appears in Run Options the next time you open the dialog.
A configuration that covers most apps:
  • One monthly and one annual subscription in a single group, with the annual plan priced so the savings are easy to see.
  • An introductory offer on both — a free trial is the most common.
Create them in your .storekit file first and iterate on the paywall locally. Once the design settles, create matching products in App Store Connect with the same product identifiers — that’s what lets the same code work in every environment.
Your first in-app purchase must be submitted for review together with a new version of your app. After that first submission is approved, later products can be submitted on their own. Plan the first submission with this in mind — it isn’t a separate, quicker path.

Before you ship

Every purchase state has been exercised in StoreKit Test — including the failure cases, not just the happy path.
Restore Purchases works from a fresh install.
One sandbox pass has been done on a real device, with no .storekit file attached, against real App Store Connect products.
The Paid Applications Agreement is active — signed, tax forms complete, bank account attached.
Product identifiers in App Store Connect exactly match the ones in your code.
Your first in-app purchase is submitted together with the new app version.
Entitlements are checked at launch rather than remembered, so a refund or an expired subscription takes access away.
Ask your agent to audit the last one for you:
Check that premium access is derived from the current StoreKit entitlements every launch, and that we’re not persisting an “is premium” flag anywhere that would survive a refund or an expired subscription.