Repository navigation
Conversation
A subscriber launches from the config the previous launch saved, without waiting on the network, and the refresh that replaces it is only scheduled once that config has been processed. A saved config that turned test mode on therefore did so on every launch, even after the dashboard setting behind it had been corrected, because the launch stopped at the test mode sheet before the refresh was scheduled. A config whose own contents put the user in test mode (a test user match or a bundle ID mismatch) is now never saved, and the config saved before it is deleted so the next launch can't start from stale real products. One saved by an earlier SDK version is deleted at launch and a fresh config is fetched instead. Test mode asked for by `testModeBehavior` or the dev server doesn't depend on the config, so those configs are still saved. `TestModeManager.evaluateTestMode` is split so the reason can be worked out without applying it. Natural Camera (app 1128) hit this on Oct 6–7: 13 subscribers stayed in test mode after the bundle ID on the dashboard was put right, and could not take photos until the app shipped with test mode disabled. 🌸 Shipped with Kanna — https://kanna.sh Co-Authored-By: Kanna <noreply@kanna.sh> Kanna-Agent: claude/opus[1m]
Config is held until the test mode sheet is closed, so every `register` call waits on it. The existing fallback covers UIKit refusing the presentation on the spot. A sheet UIKit accepts but never puts on screen had no way out, and the SDK waited on it for the rest of the launch. If the sheet hasn't started appearing three seconds after it was presented, it is taken back and the saved test mode settings are used, the same as when the presentation is refused. 🌸 Shipped with Kanna — https://kanna.sh Co-Authored-By: Kanna <noreply@kanna.sh> Kanna-Agent: claude/opus[1m]
With `testModeBehavior` left at `.automatic`, a bundle ID on the dashboard that differs from the app's turned test mode on in every build, App Store installs included. Changing that one dashboard field put an app's whole customer base in test mode: real subscriptions were replaced with simulated ones and purchases stopped reaching StoreKit. The mismatch exists so a developer's own build gets test mode with no setup, so it now only counts where a developer runs the app: simulator, development and TestFlight builds, decided by the same `DeviceHelper.isSandboxEnvironment` check the dev server uses. In an App Store build it is logged and ignored. Test users marked on the dashboard still get test mode in any build, and `.always` is unchanged. Before the first `AppTransaction` result arrives the check falls back to the receipt path, which reads as production for a Mac Catalyst development build. There test mode turns on at the first config refresh after the result lands instead of at launch. 🌸 Shipped with Kanna — https://kanna.sh Co-Authored-By: Kanna <noreply@kanna.sh> Kanna-Agent: claude/opus[1m]
|
This PR does not match any of the 1 configured review trigger rule. |
Maple review🟡 Confidence 5/10 · needs attention Prevents stale dashboard settings from trapping launches in test mode and ignores bundle ID mismatches in App Store builds. The delayed sheet fallback needs regression coverage before merging.
Findings🟠 Warning · F1 ·
|
| Change | Kind | Observable | Evidence |
|---|---|---|---|
| TestModeModal.giveUpIfNeverShown | delayed UI fallback | yes | Uses the existing Logger warning convention; completion flows through ConfigManager's existing TestModeModalClose event. |
1bbac73 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.
There was a problem hiding this comment.
ℹ️ No critical issues. One small rough edge inline and one nitpick.
Reviewed changes
I reviewed all three commits. Each fix stands on its own, and the evaluateTestMode → reasonForTestMode refactor keeps the existing behaviour.
- Saved configs that turn test mode on are dropped:
cachedConfigForLaunch()deletes a savedLatestConfigwhose own contents (test user match or bundle ID mismatch) would turn test mode on.storeAndApply(_:keepingForNextLaunch:)deletes instead of saving those configs..alwaysand DevMode configs are still saved becauseTestModeReason.comesFromConfigexcludes.testModeOption. - Backstop for an accepted sheet that never shows:
giveUpIfNeverShownwaits 3s forviewWillAppearonTestModeNavigationController. If it never comes, it takes the sheet back and callsfinish(), which is idempotent, so a late double resume is safe. - App Store builds ignore a bundle ID mismatch:
bundleIdMismatchReasononly returns a reason whenisSandboxEnvironment()is true. That is an instance closure, so overriding it in tests doesn't leak across tests the way theDevModestatic can. The existing mismatch tests still pass on the simulator becausedetectSandbox()returns"true"there.
I checked the identity timing: IdentityManager loads appUserId and aliasId from storage in init, so cachedConfigForLaunch() sees the same identity as the later evaluateTestMode call, even though fetchConfiguration and identityManager.configure() run concurrently.
ℹ️ Nitpicks
- Commit 2 has no test, but
giveUpIfNeverShown(_:modal:after:)already takes the timeout as a parameter. A Swift Testing case could call it withafter: 0on aTestModeNavigationControllerthat was never presented, then check thatonDismissfires once. That would pin the fallback cheaply.CLAUDE.mdasks for a test with each SDK change.
claude-opus-5.5 | 𝕏
…e no-show fallback Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Maple review🟢 Confidence 8/10 · likely safe to merge The revision confines ignored-mismatch warnings to config application and reuses the evaluated reason for cache retention. New timeout and appearance-guard tests address F1; no new defects found in the three files changed since the previous review. Fixed since the last review
What was checked
|
…rom-test-mode-config # Conflicts: # CHANGELOG.md
Maple review🟢 Confidence 9/10 · safe to merge Prevents cached test-mode configurations and dashboard bundle-ID mismatches from trapping production users. The latest commit only merges develop; the newly listed source and test files match the base exactly, introducing no additional SDK changes. What was checked
|
There was a problem hiding this comment.
✅ No new issues found. Both points from the last review are fixed.
Reviewed changes
I reviewed e9cb8a2 and the merge from develop. The merge only moves CHANGELOG entries.
- The ignored-mismatch warning now logs once per config:
reasonForTestMode(config:options:)no longer has side effects. OnlyevaluateTestModepasseswarnAboutIgnoredMismatch: true, and it now returns the reason.processConfigworks out the reason once and uses that one value both to apply test mode and forkeepingForNextLaunch. evaluateTestModenow runs beforestoreAndApplyinprocessConfig: I checked that this order is safe.evaluateTestModeandclearTestModeStatedon't read anythingstoreAndApplysets (triggers, paywall variants, IP collection). Both run synchronously before the firstawait, so nothing can see the state between them.- Added tests for
giveUpIfNeverShown: one test checks that a sheet that never appears reports back exactly once. The other checks that a sheet whoseviewWillAppearalready ran is left alone. Both passafter: 0and run on the@MainActor,.serializedsuite.
claude-opus-5.5 | 𝕏

Changes in this pull request
Three fixes for the test mode incident on Natural Camera (app 1128), one per commit so any of them can be dropped.
What happened
On Oct 6 the app's bundle ID was changed on the dashboard. With
testModeBehaviorat its default, every App Store install went into test mode: 46 of 54 active users over the next 17 hours.configStatenever published and everyregistercall waited for the rest of the launch. The app saves photos insideregister, so no photo saved.processConfigreturns. All 13 affected subscribers were stuck until the app shipped a build with test mode set to.never.It isn't only this app
I went through
testModeModal_openevents for every app over the last 30 days. Two more apps have App Store users in test mode. Their names are left out because this repo is public; Jake has them.How to tell the cases apart in the events:
testModeModal_openfrom a device whose launchdevice_attributeshave$isSandbox: "false"testModeModal_openwith notestModeModal_close, andstaticConfigBuildIdblank on every event of that launchsubscriptionStatus_didChangetoINACTIVEright aftertestModeModal_close1. Never save or launch from a config that turns test mode on
LatestConfig, and the config saved before it is deleted.testModeBehavior = .alwaysor the dev server doesn't depend on the config, so those configs are still saved.TestModeManager.evaluateTestModeis split so the reason can be worked out without applying it. Behaviour is unchanged.The cost: a user in test mode fetches config from the network on every launch. Test mode already needs the network for its products.
2. Stop waiting on a test mode sheet that never appears
#534 handles UIKit refusing the presentation on the spot. If UIKit accepts the sheet but it still hasn't started appearing after 3 seconds, the sheet is taken back and the saved settings are used, the same as the refusal path. This is a backstop; I don't have a reproduction for this path.
3. Ignore a bundle ID mismatch in App Store builds
A bundle ID mismatch now only turns test mode on where a developer runs the app (simulator, development, TestFlight), using the same
DeviceHelper.isSandboxEnvironmentcheck as the dev server. In an App Store build it is logged and ignored. Test users marked on the dashboard still get test mode in any build, and.alwaysis unchanged.This is a behaviour change and the online docs still describe the old behaviour. One known gap: before the first
AppTransactionresult, a Mac Catalyst development build reads as production, so test mode there turns on at the first config refresh instead of at launch.On #534 and #540
#534 is the right fix for the layout that broke here:
topPresenterputs the sheet above the controller the root is presenting instead of having it dropped. #540 covers the multi-scene and multi-window case and this PR doesn't touch it.One case neither closes: if the SDK presents from the app's root before the app presents its own main UI from that root, the app's presentation is the one UIKit refuses. Paywalls avoid this by presenting from the SDK's own window (
createPresentingWindowIfNeeded). Presenting the sheet the same way would close it. Not done here.Testing
ConfigManagerTestsandTestModeManagerTests. I ranConfigManagerTests,ConfigManagerEarlyPublishTests,TestModeManagerTestsandTestModeModalTestson an iOS simulator: 60 tests passed. With theConfigManagerchange reverted, the two cache tests fail (the subscriber test fails ongetConfigCalled == false, which is the bug).TestModeManagerTests, not run. It compiles.Checklist
CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.swiftlintin the main directory and fixed any issues.🌸 Shipped with Kanna — an open-source workspace for all your coding agents. Written by
claude/opus[1m].