Skip to content

feat: add teleport support - #542

Open
chroxify wants to merge 7 commits into
developfrom
christo/web-purchase-as-purchase
Open

chroxify wants to merge 7 commits into
developfrom
christo/web-purchase-as-purchase

Conversation

@chroxify

@chroxify chroxify commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the SDK side of teleport, which lets a native paywall send the purchase to a Superwall checkout page in the browser, and completes that web purchase like an App Store purchase.

  • Return link: scheme://superwall/return and the …/app-link/superwall/return universal link bring the user back without being tracked as deepLink_open, so the paywall isn't dismissed. They're tracked as teleport_return instead, with a reason (purchased / closed) taken from the link.
  • Waiting screen: TeleportReturnCover covers the paywall's waiting screen with a native picture of the paywall, so the waiting screen never shows in the app switcher snapshot or on return. It checks the checkout in the background (teleport_watch_start / teleport_watch_end) and tells the page when the user is back through hideCheckoutWaitingScreen({ reason }).
  • Purchase completion: stripe_checkout_complete with should_dismiss finishes the redemption as a purchase. transaction_complete fires, free-trial start is tracked, and the paywall dismisses with .purchased. Without should_dismiss it keeps the restore behaviour.
  • Attribution: paywall_open carries a presentation_id. teleport_open is tracked when the checkout page opens.
  • Redemption polling fix: a checkout reported while an older pending checkout was still being polled was never polled itself, so the spinner stayed up. It now waits for the running poll, then polls its own checkout, and an older poll stops when a newer checkout replaces it.
  • Non-production environments: SubscriptionsApiEnvironmentScript tells the paywall to use the staging or local subscriptions-api when the SDK runs with .developer or .local. Production is unchanged.

Teleport is gated to SDK 4.18.0+ in paywall.js. Older SDKs keep the existing external web checkout.

Checklist

  • All unit tests pass. (1,333 of 1,337 pass. The other 4, in CustomerCenterSheetOwnershipTests and ProductsFetcherSK2Tests, fail the same way on develop.)
  • All UI tests pass.
  • Demo project builds and runs on iOS.
  • Demo project builds and runs on Mac Catalyst.
  • Demo project builds and runs on visionOS.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run swiftlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

🤖 Generated with Claude Code

@chroxify
chroxify requested a review from yusuftor October 8, 2026 14:25
@greptile-apps

greptile-apps Bot commented Oct 8, 2026

Copy link
Copy Markdown

This PR does not match any of the 1 configured review trigger rule.

@maple-review-bot

maple-review-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Maple review

🔴 Confidence 2/10 · do not merge
Purchase recovery loses completion semantics, default confirmation can block completion, and return-cover lifecycle races lack regression tests.
quality 50/100 · 5 warnings · tests partial · risk high · 3/4 new units observable

Adds browser checkout teleport, native return-screen handling, and purchase-style web redemption. The purchase and app-lifecycle paths have confirmed defects that need fixes before merging.

  • DeepLinkRouter handles teleport return links separately from ordinary deep links.
  • TeleportReturnCover manages checkout snapshots and background status checks.
  • WebEntitlementRedeemer adds purchase completion and waits for active redemption polls.
  • SubscriptionsApiEnvironmentScript selects staging and local checkout environments.

Findings

🟠 Warning · F1 · Interrupted grace period leaves revealing permanently set

correctness · Sources/SuperwallKit/Paywall/View Controller/TeleportReturnCover.swift:431-438

When the app becomes inactive during the 350 ms grace period, this guard returns without clearing revealing. On subsequent activations, revealAfterGrace exits at its initial guard, leaving the static paywall picture over the waiting screen until a return link arrives or the paywall is dismissed.

Clear the matching reveal token when the grace period is interrupted so the next activation can retry, and add a lifecycle regression test.
🟠 Warning · F2 · revealAfterGrace removes the cover during return-link cleanup

correctness · Sources/SuperwallKit/Paywall/View Controller/TeleportReturnCover.swift:449-452

A return link arriving during the 250 ms fade reuses the same cover and starts uncover, but the existing animation continues. This identity check still passes, so its completion removes the cover and clears uncovering before the page finishes hiding the waiting screen.

Invalidate the fade completion and cancel the animation when uncovering starts, restoring the cover's opacity until page cleanup finishes; test a return link arriving mid-fade.
🟠 Warning · F3 · .purchase still executes restore-only entitlement validation

correctness · Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift:500-501

handleRedemptionSuccess calls processEntitlements before this purchase branch; that helper requires entitlements for every product on the paywall and sends restore_complete or restoreFail. Buying one tier on a paywall offering distinct entitlements therefore shows a restore-failed alert; with the default confirmation setting, presentAlert rejects the success alert and its afterRedeem callback never completes the purchase.

Pass the completion mode into entitlement processing and skip restore-specific validation, messages, and alerts for purchases.
🟠 Warning · F4 · Recovery polling loses StripeCheckoutCompletion.purchase

correctness · Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift:855

After a purchase poll fails with a request error, pending state retains only checkout and product IDs. Foreground and paywall-open recovery call this method without completion, so a recovered purchase runs .restore, omits transaction_complete, and can dismiss as restored even when the original purchase specified shouldDismiss: false.

Retain the completion mode and dismissal flag in pending checkout state and pass them through recovery polls.
🟠 Warning · F5 · updatePaywall replaces the ID already used by loading events

correctness · Sources/SuperwallKit/Paywall/Request/PaywallRequestManager.swift:114

On an uncached request, getRawPaywall assigns a presentation ID before tracking response loading, and product loading uses that paywall. The fresh-request path then calls updatePaywall and replaces the ID, so loading and presentation events from the same lifecycle no longer correlate.

Preserve the ID for the originating fresh request and allocate a new ID only for reused paywalls; test loading-to-presentation ID consistency.
🤖 Prompt to fix all 5 findings with an AI agent
Findings from an automated review of commit 61aff7f9f4ba5a69e811c7cdd9410ead9ca13f3b. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F1 · Warning · correctness · Sources/SuperwallKit/Paywall/View Controller/TeleportReturnCover.swift:431-438
Interrupted grace period leaves `revealing` permanently set
When the app becomes inactive during the 350 ms grace period, this guard returns without clearing `revealing`. On subsequent activations, `revealAfterGrace` exits at its initial guard, leaving the static paywall picture over the waiting screen until a return link arrives or the paywall is dismissed.
Suggested fix: Clear the matching reveal token when the grace period is interrupted so the next activation can retry, and add a lifecycle regression test.

---

F2 · Warning · correctness · Sources/SuperwallKit/Paywall/View Controller/TeleportReturnCover.swift:449-452
`revealAfterGrace` removes the cover during return-link cleanup
A return link arriving during the 250 ms fade reuses the same cover and starts `uncover`, but the existing animation continues. This identity check still passes, so its completion removes the cover and clears `uncovering` before the page finishes hiding the waiting screen.
Suggested fix: Invalidate the fade completion and cancel the animation when uncovering starts, restoring the cover's opacity until page cleanup finishes; test a return link arriving mid-fade.

---

F3 · Warning · correctness · Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift:500-501
`.purchase` still executes restore-only entitlement validation
`handleRedemptionSuccess` calls `processEntitlements` before this purchase branch; that helper requires entitlements for every product on the paywall and sends `restore_complete` or `restoreFail`. Buying one tier on a paywall offering distinct entitlements therefore shows a restore-failed alert; with the default confirmation setting, `presentAlert` rejects the success alert and its `afterRedeem` callback never completes the purchase.
Suggested fix: Pass the completion mode into entitlement processing and skip restore-specific validation, messages, and alerts for purchases.

---

F4 · Warning · correctness · Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift:855
Recovery polling loses `StripeCheckoutCompletion.purchase`
After a purchase poll fails with a request error, pending state retains only checkout and product IDs. Foreground and paywall-open recovery call this method without `completion`, so a recovered purchase runs `.restore`, omits `transaction_complete`, and can dismiss as restored even when the original purchase specified `shouldDismiss: false`.
Suggested fix: Retain the completion mode and dismissal flag in pending checkout state and pass them through recovery polls.

---

F5 · Warning · correctness · Sources/SuperwallKit/Paywall/Request/PaywallRequestManager.swift:114
`updatePaywall` replaces the ID already used by loading events
On an uncached request, `getRawPaywall` assigns a presentation ID before tracking response loading, and product loading uses that paywall. The fresh-request path then calls `updatePaywall` and replaces the ID, so loading and presentation events from the same lifecycle no longer correlate.
Suggested fix: Preserve the ID for the originating fresh request and allocate a new ID only for reused paywalls; test loading-to-presentation ID consistency.
What was checked
  • Production and custom environments inject nothing; SubscriptionsApiEnvironmentScriptTests covers each default.
  • Purchase tests cover shouldDismiss: false and disabled automatic dismissal.
  • presentAlert rejects overlapping alerts, confirming the restore-validation conflict.
Observability coverage: 3 of 4 changes observable
Change Kind Observable Evidence
Teleport return-link routing entrypoint yes Tracks TeleportReturn through the SDK's existing analytics pipeline.
Teleport browser transition and return cover background yes TeleportOpen analytics and Logger lifecycle messages follow existing SDK conventions.
CheckoutStatusCheck status requests outbound no Raw URLSession requests have no spans; lifecycle logging surrounds the watcher, following the SDK's existing non-OTel convention.
Web purchase completion operation yes Retains redemption and free-trial analytics and sends transaction_complete to the paywall.

61aff7f · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 inline notes from Maple's review. The score and summary are in the review comment above.

Comment thread Sources/SuperwallKit/Paywall/View Controller/TeleportReturnCover.swift Outdated
Comment thread Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift
Comment thread Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift
for request: PaywallRequest
) async -> Paywall {
var paywall = paywall
paywall.presentationId = UUID().uuidString

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

updatePaywall replaces the ID already used by loading events

F5 · Warning · correctness

On an uncached request, getRawPaywall assigns a presentation ID before tracking response loading, and product loading uses that paywall. The fresh-request path then calls updatePaywall and replaces the ID, so loading and presentation events from the same lifecycle no longer correlate.

Preserve the ID for the originating fresh request and allocate a new ID only for reused paywalls; test loading-to-presentation ID consistency.
🤖 Prompt to fix with an AI agent
In `Sources/SuperwallKit/Paywall/Request/PaywallRequestManager.swift:114`: `updatePaywall` replaces the ID already used by loading events.

On an uncached request, `getRawPaywall` assigns a presentation ID before tracking response loading, and product loading uses that paywall. The fresh-request path then calls `updatePaywall` and replaces the ID, so loading and presentation events from the same lifecycle no longer correlate.

Suggested fix: Preserve the ID for the originating fresh request and allocate a new ID only for reused paywalls; test loading-to-presentation ID consistency.

Verify the problem exists at that location before changing it, and keep the fix to those lines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't fix

The premise doesn't hold: presentationId is assigned in exactly one place, updatePaywall. getRawPaywall never sets one, so the response and product loading events for a fresh request carry no presentation ID rather than a different one. The ID is allocated once per request, after loading, so every event from presentation onwards shares it, which is the intent. Reused cached paywalls get a fresh ID per request for the same reason.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

A paywall view controller that is reused can report a purchase from an earlier presentation as .purchased when the user simply closes it. There is also a background-task contract issue in TeleportReturnCover. Details are inline.

Reviewed changes

I reviewed the full teleport feature: return links, the waiting-screen cover, web purchase completion, redemption polling, and the environment script.

  • Return links: scheme://superwall/return and the app-link universal links skip deepLink_open and track teleport_return with a reason. They also notify the paywall through .superwallReturnLinkOpened.
  • TeleportReturnCover: before opening the browser, it puts a still of the paywall in front, then has the page draw the waiting screen. In the background it checks the checkout and covers the waiting screen for the app switcher snapshot. On return it calls hideCheckoutWaitingScreen({ reason }).
  • Web purchase as purchase: stripe_checkout_complete with should_dismiss sends transaction_complete to the paywall. It dismisses as .purchased only when both shouldDismiss and automaticallyDismiss are true.
  • Decline after purchase → purchased: completedPurchaseProduct plus resolvedDismissal turn a .declined close that follows an App Store or web purchase into .purchased.
  • Redemption polling fix: a checkout that completes while another poll is running now waits and then polls its own checkout. A stale poll stops once a newer checkout replaces the pending state.
  • Attribution and environment: presentationId is now generated for each request, and paywall_open carries the presentation payload. SubscriptionsApiEnvironmentScript points paywall.js at staging or local for non-production SDKs.

ℹ️ No changelog entry for the redemption spinner fix

The PR description lists a customer-visible bug fix: the loading spinner stayed up when a checkout completed while an older pending checkout was still being polled. The ### Fixes section of CHANGELOG.md doesn't mention it. CLAUDE.md asks for bug fixes to go in the changelog.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5.5 | 𝕏

Comment thread Sources/SuperwallKit/Paywall/View Controller/TeleportReturnCover.swift Outdated
Comment thread Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift Outdated
@maple-review-bot

maple-review-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Maple review

🔴 Confidence 3/10 · risky as written
F5 remains reproducible in the fresh-request path, and the corrected return-cover timing paths have no regression tests.
quality 80/100 · 2 warnings · tests partial · risk high · 2/3 new units observable

Adds teleport return handling and completes web checkouts as purchases. This head fixes F1–F4, but F5 still breaks presentation-ID correlation; the return-cover lifecycle fixes also lack regression coverage. Address those before merging.

  • PendingStripeCheckoutPollState preserves purchase completion through recovery.
  • processEntitlements skips restore-only validation for purchases.
  • uncover cancels the return-cover fade before waiting-screen cleanup.

Findings

🟠 Warning · F6 · TeleportReturnCover lifecycle fixes have no regression tests

tests · Sources/SuperwallKit/Paywall/View Controller/TeleportReturnCover.swift:441-445

The interrupted-grace and mid-fade return-link fixes have no tests: no test references TeleportReturnCover or revealAfterGrace. These timing paths caused F1 and F2, but the added purchase and message-handler tests cannot detect either regression.

Add Swift Testing coverage for reactivation after an interrupted grace period and a return link during the fade; inject the lifecycle state and timing dependencies as needed.
🤖 Prompt to fix this finding with an AI agent
Findings from an automated review of commit a661d41d1eee5a75b82340ce4dd765147c212141. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F6 · Warning · tests · Sources/SuperwallKit/Paywall/View Controller/TeleportReturnCover.swift:441-445
`TeleportReturnCover` lifecycle fixes have no regression tests
The interrupted-grace and mid-fade return-link fixes have no tests: no test references `TeleportReturnCover` or `revealAfterGrace`. These timing paths caused F1 and F2, but the added purchase and message-handler tests cannot detect either regression.
Suggested fix: Add Swift Testing coverage for reactivation after an interrupted grace period and a return link during the fade; inject the lifecycle state and timing dependencies as needed.

Still open from earlier reviews

Fixed since the last review

  • ✅ F1 · Interrupted grace period leaves revealing permanently set
  • ✅ F2 · revealAfterGrace removes the cover during return-link cleanup
  • ✅ F3 · .purchase still executes restore-only entitlement validation
  • ✅ F4 · Recovery polling loses StripeCheckoutCompletion.purchase
What was checked
  • processEntitlements gates restore validation on .restore; tiered-paywall tests exercise both paths.
  • Foreground and paywall-open recovery pass persisted completion; legacy decoding defaults to .restore.
  • revealAfterGrace clears its token before lifecycle guards; uncover cancels the fade.
Observability coverage: 2 of 3 changes observable
Change Kind Observable Evidence
Teleport return-link handling SDK entrypoint yes DeepLinkRouter tracks TeleportReturn with paywall information and return reason using existing SDK analytics.
Web checkout purchase completion and recovery SDK purchase workflow yes Uses existing redemption tracking, FreeTrialStart tracking, and transactionComplete webview messaging.
Checkout background status checks background polling no TeleportReturnCover uses existing Logger.debug conventions; CheckoutStatusCheck has no per-request or per-watch spans.

a661d41 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ The earlier findings are fixed. One minor note is inline.

Reviewed changes

This review covers the fix commit a661d41 and the develop merge that landed after the previous Pullfrog review.

  • Purchase scoped to its presentation: viewDidDisappear clears completedPurchaseProduct. A view controller that is reused, whether through getPaywall or the cache, no longer reports an older purchase as .purchased.
  • Synchronous expiration handler: TeleportReturnCover now covers the screen and ends the background task inside MainActor.assumeIsolated. Before, it hopped to a Task.
  • Reveal and return-link interplay: uncover() cancels a reveal fade that is still running and sets the cover back to full opacity. The fade's completion checks finished and uncovering. revealing is cleared after the grace period, so a reveal that gets interrupted can run again on the next activation.
  • Purchase skips restore validation: processEntitlements only checks entitlements against every product on the paywall when the completion is .restore. So when a paywall's tiers carry different entitlements, buying one tier no longer shows the restore failed alert.
  • Recovery keeps the completion mode: PendingStripeCheckoutPollState now stores StripeCheckoutCompletion, so recovery on foreground or paywall open finishes a purchase as a purchase. State saved by older SDK versions still decodes as .restore.
  • Duplicate checkout reports: handleStripeCheckoutComplete exits early when the same checkout context has already been redeemed, and it stops waiting once an in-flight poll redeems it.
  • Changelog: added entries for the spinner fix and the tiered restore failure fix. Also extracted DeepLinkRouter.handleReturnLink, a refactor with no behavior change.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5.5 | 𝕏

Comment thread Examples/Basic/Basic.xcodeproj/project.pbxproj Outdated

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 inline note from Maple's review. The score and summary are in the review comment above.

@maple-review-bot

maple-review-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Maple review

🔴 Confidence 4/10 · risky as written
The signing correction is contained, but presentation-ID correlation remains broken and cover lifecycle fixes still lack regression tests.
quality 80/100 · 2 warnings · tests partial · risk high

The latest commit restores the Basic example’s signing settings to the base configuration and introduces no new defects. Teleport still needs attention: F5 and F6 remain open at this head.

Still open from earlier reviews

What was checked
  • Both Basic signing configurations match the base; the incremental diff only clears DEVELOPMENT_TEAM.
  • Fresh requests still replace the presentation ID after response-loading tracking, confirming F5.
  • Tests reference TeleportReturnCover.Watch but do not exercise cover lifecycle timing, confirming F6.

52d39f8 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@maple-review-bot

maple-review-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Maple review

🟡 Confidence 5/10 · needs attention
F5 remains in the fresh-request path; F6 lacks a test holding page cleanup pending beyond the fade completion.
quality 80/100 · 2 warnings · tests partial · risk medium

Adds test seams and lifecycle tests for TeleportReturnCover. F6 is only partially addressed: the mid-fade test finishes before the old animation completes, so it does not protect that cleanup race. F5's presentation-ID mismatch remains unchanged; both findings stay open.

  • TeleportReturnCover injects timing and application/network operations for tests.
  • testRevealAfterInterruptedGrace exercises a second activation after interrupted grace.

Still open from earlier reviews

What was checked
  • Default Timing values match the previously hard-coded production constants.
  • Xcode registers TeleportReturnCoverTests.swift in the test target.

d67aff8 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No production issues. The new TeleportReturnCover tests share the global NotificationCenter, so they can flake. Details are inline.

Reviewed changes

This review covers d67aff8, the only commit with code since the previous Pullfrog review. (52d39f8 only reverted the Basic example's signing team.)

  • Injectable timing and environment: TeleportReturnCover moves its constants into a Timing struct and its UIApplication, snapshot and network calls into an Environment struct. Both default to the previous behavior, so production is unchanged.
  • Test hooks: added isCovering and coverAlpha, read-only accessors for tests.
  • New TeleportReturnCoverTests: drives the cover with lifecycle notifications. It checks that an interrupted grace period still allows a later reveal, and that a return link arriving mid-fade restores full opacity until the page removes the waiting screen.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5.5 | 𝕏

Comment thread Tests/SuperwallKitTests/Paywall/View Controller/TeleportReturnCoverTests.swift Outdated
Comment thread Tests/SuperwallKitTests/Paywall/View Controller/TeleportReturnCoverTests.swift Outdated
Comment thread Tests/SuperwallKitTests/Paywall/View Controller/TeleportReturnCoverTests.swift Outdated
@maple-review-bot

maple-review-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Maple review

🟡 Confidence 6/10 · needs attention
Lifecycle regressions now have isolated tests, but updatePaywall still replaces the presentation ID already used by loading events.
quality 90/100 · 1 warning · tests partial · risk high

Teleport adds browser checkout completion as a native purchase. The latest revision isolates lifecycle tests with per-fixture notification centers and addresses F6; F5's loading-to-presentation ID mismatch remains unresolved, so the pull request still needs correction.

  • TeleportReturnCover.Environment injects the notification center while preserving the production default.
  • TeleportReturnCoverTests isolates lifecycle notifications and removes the ineffective no-cover test.

Still open from earlier reviews

Fixed since the last review

  • ✅ F6 · TeleportReturnCover lifecycle fixes have no regression tests
What was checked
  • Incremental diff preserves production notifications through the .default environment value.
  • Regression tests exercise interrupted grace and a return link during fading.
  • Fresh requests still replace the ID after response and product loading events.

791ae62 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. All three earlier test findings are fixed.

Reviewed changes

This review covers 791ae62, the only commit since the previous Pullfrog review.

  • Injectable notification center: added TeleportReturnCover.Environment.notificationCenter, which defaults to .default. All four observers now register on it, so production behavior is unchanged.
  • Isolated test fixtures: each Fixture creates its own NotificationCenter(), and post(_:to:userInfo:) sends to that fixture's center. Tests running in parallel in this suite and in DeepLinkRouterTests can no longer trigger each other's covers.
  • Test cleanup: removed the tautological testReturnLinkWithoutCover and the unused Fixture.applicationState.

Pullfrog  | View workflow run | Using claude-opus-5.5 | 𝕏

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant