Skip to content

Survive the login bounce with an intended url - #126

Merged
anilcancakir merged 12 commits into
mainfrom
feature/intended-url-after-login
Sep 10, 2026
Merged

Survive the login bounce with an intended url#126
anilcancakir merged 12 commits into
mainfrom
feature/intended-url-after-login

Conversation

@anilcancakir

@anilcancakir anilcancakir commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

What

A deep link that lands on a signed-out device now survives the login bounce, and the magic_notifications pin follows that package to ^0.3.0.

Why the intended url

A link into a protected screen on a device nobody is signed into used to bounce to login and then land on the home route, dropping the destination. EnsureAuthenticated.redirectTarget now records the requested location via MagicRouter.setIntendedUrl before bouncing, and NavigatesRoutes.navigateHome reads it back with pullIntendedUrl once they authenticate, falling back to MagicStarterConfig.homeRoute().

Nothing is recorded for the login route itself or for any other guest-only auth route (register, forgot-password, reset-password, two-factor-challenge, otp), since a bounced visitor cannot use one of those as a destination either. All five post-auth navigations now go through navigateHome().

Known limit, stated rather than discovered later: redirectTarget only ever sees state.matchedLocation, which carries no query string, so a recorded intent loses any ?token=... the original link carried.

Why the pin has to move

magic_notifications 0.3.0 is where a push tapped on a COLD START stops depending on provider order, which was decided by install order rather than by anything a consumer chose. ^0.2.0 is >=0.2.0 <0.3.0, so it excludes that release: leaving the line alone would hold every adopter of this starter on the version where a cold tap silently opens the wrong screen.

This is the step in a release train that gets skipped, because nothing fails until an adopter resolves the published graph and finds the fix absent.

Order

Depends on fluttersdk/magic#152 only, and not for the code: skill_reference_stamp_test.dart reads plugin-starter.md from magic's DEFAULT branch, which this repo's CI clones with git clone --depth 1 and no --branch (ci.yml:37-40). That stamp is still alpha.23; #152 carries alpha.27. Sibling working trees goes green on a re-run once it merges.

The magic_notifications pin bump that used to sit here is gone, so nothing in this PR waits on a publish.

Testing

flutter analyze clean.

Wave 1 of the deep-link plan.

EnsureAuthenticated bounced an unauthenticated visitor to login and recorded
nothing, so a cold start at a deep link lost its destination. It now records
the location through MagicRouter.setIntendedUrl, which existed with zero
callers, and a new NavigatesRoutes.navigateHome reads it back once the user
authenticates. Nothing is recorded for the login route or any other
guest-only auth route, since a visitor bounced off one of those cannot use
it as a destination either.

All five post-auth navigations now go through navigateHome, which makes it
the single seam. Known limit, recorded in the docblock: redirectTarget only
ever sees state.matchedLocation, which carries no query string.
Carries the intended-URL replay: a deep link that lands on a signed-out device
returns to its destination after login.
The two starter:* banners read magicStarterVersion, and its own test compares
it against pubspec.yaml for exactly this reason: a literal nothing compares
against drifts silently, and this one was already twenty-four releases stale
once before.
//host/path starts with a slash and is a different origin, so the
leading-slash test alone admitted it. Nothing reachable can store one today,
since only a router location is ever recorded, which is why the extra
condition is cheap enough to keep.
…s version

The reference an agent adopting this package reads lives in the magic repo,
not here: .pubignore keeps CLAUDE.md and .claude/ out of the published
archive, so pub.dev ships doc/ and README.md and nothing else an agent is
pointed at. That reference is versioned by magic's releases rather than by
this package's, and it drifted far enough to still document the notification
UI that had moved out of this package entirely.

A test compares its first-line stamp against this pubspec's version. It cannot
run in CI, which clones no siblings, so it skips there instead of failing;
releases are cut locally and that is where it fires. release.md carries the
same requirement in prose for anyone running the suite elsewhere.
0.3.0 is where a push tapped on a cold start stops depending on which order the
consumer's provider list happens to be in, which was decided by install order
rather than by anything a consumer chose. `^0.2.0` is `>=0.2.0 <0.3.0`, so it
excludes that release: leaving this line alone would hold every adopter of this
starter on the version where a cold tap silently opens the wrong screen.

This is the step in a release train that gets skipped, because nothing fails
until an adopter resolves the published graph and finds the fix absent.
@anilcancakir

Copy link
Copy Markdown
Contributor Author

The Published graph check is red, and it is red for the reason this PR exists rather than for a defect. Reproduced locally by resolving without the sibling overrides, exactly as that job does:

Because magic_starter depends on magic_notifications ^0.3.0 which doesn't match
any versions, version solving failed.

0.3.0 is not on pub.dev yet; it is fluttersdk/magic_notifications#27, which has to be published before this merges. That is intrinsic to a release train rather than something to work around: this package's alpha.27 has to carry ^0.3.0 at the moment it publishes, or it ships pointing at the notifications release where a cold-start push tap still depends on provider order. Raising the pin before the dependency exists is the only ordering that produces a correct artifact, and the check is red for exactly the window in between.

It goes green on a re-run once 0.3.0 is published. Do not downgrade the constraint to make it pass, which is what pub's own suggestion in that log proposes.

The other two checks are unaffected: Sibling working trees resolves against the local checkouts where 0.3.0 exists.

@anilcancakir

Copy link
Copy Markdown
Contributor Author

Correcting my previous comment: Sibling working trees is NOT unaffected, and it is red too. Same ordering cause, different mechanism.

Expected: contains 'magic_starter v0.0.1-alpha.27'
  Actual: '<!-- magic_starter v0.0.1-alpha.23 | Updated: 2026-08-29 -->'

skill_reference_stamp_test.dart reads ../magic/skills/magic-framework/references/plugin-starter.md, and this job supplies that sibling with git clone --depth 1 and no --branch (ci.yml:37-40), so it gets magic's DEFAULT branch. The alpha.27 stamp is on fluttersdk/magic#152 and has not merged, so master still carries alpha.23.

Worth noting because it changes where this test can fire: magic_notifications and magic_deeplink have the same gate and it SKIPS in their CI, which clones no siblings. This repo's does clone them, so here the gate is live in CI rather than only at release time.

So both red checks on this PR clear in the same order, and neither needs a code change:

  1. Make the plugin references true again, and stamp them for the releases going out magic#152 merges -> Sibling working trees goes green on a re-run
  2. magic_notifications 0.3.0 publishes -> Published graph goes green

Nothing in this branch needs editing for either.

@kodizm

kodizm Bot commented Sep 9, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

The intended-url logic itself is sound and tested, but two things in this branch are red right now: the package no longer resolves at all, and the new stamp test fires in the blocking CI job rather than skipping there.

Critical

pubspec.yaml:60magic_notifications: ^0.3.0 names a release that does not exist: pub.dev's latest is 0.2.0, so the package is unresolvable for anyone, including this repo's own tooling. The PR's ordering note covers the intent, but the branch also carries the version bump to 0.0.1-alpha.27 and a dated changelog heading, so nothing between here and a publish blocks the broken graph - and the published CI job that would catch it is continue-on-error: true (.github/workflows/ci.yml:100). Evidence, in this checkout: flutter pub getBecause magic_starter depends on magic_notifications ^0.3.0 which doesn't match any versions, version solving failed.

test/skill_reference_stamp_test.dart:33 — this fails the blocking siblings job, not skips it. Its comment says CI "clones no siblings", but .github/workflows/ci.yml:38 clones magic into ../magic beside the magic_starter checkout, which is exactly the path the test probes, so existsSync() is true and the assertion runs. fluttersdk/magic@master's first line is <!-- magic_starter v0.0.1-alpha.23 | Updated: 2026-08-29 -->, so it will fail on contains('magic_starter v0.0.1-alpha.27') until that other repo is updated - which by the stated release order cannot happen before this one ships. (Verified by reading the workflow and fetching the file's first line; I could not run the suite here, see below.)

Minor

lib/src/middleware/ensure_authenticated.dart:53 — the intent is recorded on any bounce, including one caused by logout or a passive 401 (the router re-runs redirects on the auth state notifier). Sign out on /teams/settings, hand the device over, and the next person to sign in - or the same person tapping "continue as guest", which now also calls navigateHome() - lands on the previous session's page rather than home. Correctness/UX rather than data exposure, since the destination refetches under the new token, but it is a behaviour change worth being deliberate about.

lib/src/middleware/ensure_authenticated.dart:63_isGuestRoute is unreachable through the shipped route table: auth_routes.dart:19 registers the whole auth group under middleware: ['guest'], so EnsureAuthenticated never sees /auth/register or its siblings. Harmless as defence for a host that adds auth globally, but the two tests covering it call redirectTarget directly and so do not show the router ever reaching it.

doc/basics/authentication.md:55 — still says the login flow "navigates to MagicStarterConfig.homeRoute()", which is now only the fallback. CLAUDE.md's post-change checklist asks for doc/ to be synced with the change.

Tests

navigates_routes_test.dart covers all three navigateHome branches (intent, no intent, non-path value) and ensure_authenticated_test.dart covers record/skip/authenticated; nothing covers the five controllers now routing through navigateHome(), so the wiring itself is untested.

Checks I ran

  • flutter pub get → failed, magic_notifications ^0.3.0 matches no published version; no flutter analyze or flutter test was possible in this checkout as a result.
  • Confirmed setIntendedUrl / pullIntendedUrl / hasIntendedUrl and MagicRouter.reset() clearing _intendedUrl all exist in published magic 0.0.6, so the magic: ^0.0.6 floor is correct for this change.
  • Fetched fluttersdk/magic@master's skills/magic-framework/references/plugin-starter.md first line → stamped v0.0.1-alpha.23.

@anilcancakir

Copy link
Copy Markdown
Contributor Author

Dropped the magic_notifications: ^0.3.0 pin bump; back to ^0.2.0.

Raising it before 0.3.0 was published is what made Published graph red, and it was the wrong shape anyway: a publish is not on the critical path, so this PR should be the intended-url change and nothing else. The pin moves when the release is actually cut.

That leaves one red check, Sibling working trees, and it needs no change here either: it reads plugin-starter.md from magic's DEFAULT branch (git clone --depth 1, no --branch, ci.yml:37-40), where the stamp is still alpha.23. fluttersdk/magic#152 carries alpha.27, so this goes green on a re-run once that merges.

@kodizm

kodizm Bot commented Sep 9, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of the one commit since ecf6ffe: it is a clean revert of the magic_notifications pin, and it resolves the Critical I raised - the package builds and resolves against published packages again, with no new findings in the code.

Only pubspec.yaml changed, and only to put the constraint and its comment back to ^0.2.0. Nothing else in the branch referenced 0.3.0, so the revert leaves no dangling claim: the alpha.27 changelog entry is about the intended-url change alone, and no doc mentions the pin.

Minor

PR title - still reads "and follow magic_notifications to ^0.3.0", and the ## Order section still lists magic_notifications#27 as a blocker for this PR. Neither is true after the revert, and a squash merge would take that title verbatim into main's history. Metadata only, no code impact.

Still open from my earlier review

The three Minors on the intended-url logic itself are unaddressed and unanswered: intent recorded on a logout-triggered bounce (ensure_authenticated.dart:53), _isGuestRoute unreachable through the shipped route table (ensure_authenticated.dart:63), and doc/basics/authentication.md:55 still saying login "navigates to MagicStarterConfig.homeRoute()" when that is now only the fallback (verified still present). Take or leave them; none blocks a merge.

The Sibling working trees red check needs no change here - the explanation given (magic's default branch still stamped alpha.23 until fluttersdk/magic#152 merges) matches what I see: the stamp test skips in this container because no sibling checkout exists, and its guard is existsSync().

Tests

Unchanged by this commit. navigates_routes_test.dart and ensure_authenticated_test.dart still cover the change; the five controllers now routing through navigateHome() still have no test on that wiring.

Checks I ran

  • flutter pub get → resolves cleanly now, no path overrides in play; pubspec.lock and package_config.json both show magic_notifications-0.2.0 from pub.dev. This is the Critical from my last review, confirmed fixed.
  • flutter analyzeNo issues found! (18.8s). Confirms nothing in the intended-url code needed a 0.3.0 API.
  • flutter test test/http/controllers/concerns/navigates_routes_test.dart test/middleware/ensure_authenticated_test.dart test/skill_reference_stamp_test.dart+9 ~1: All tests passed!; the skip is the stamp test, guarded on the absent sibling checkout.
  • Full flutter test was not run - it exceeded my time budget and I killed it, so the other ~107 test files are unverified by me.

Raised in review. Flipping the auth state makes go_router re-run its redirects
while the app is still sitting on the protected route, so `EnsureAuthenticated`
writes that route down as somewhere to return to, in the gap between
`Auth.logout()` and the navigation to login. It belongs to the session that just
ended.

Sign out on /teams/settings, hand the device over, and the next person to sign
in lands there rather than on home. The destination refetches under the new
token so this is not an exposure, but it is somebody else's page and nobody
asked for it. The account-deletion path had the sharper version of the same
thing: it sent the next sign-in to a deleted account's settings screen.

Read-and-discard through `pullIntendedUrl`, which is the one-time read;
`MagicRouter` exposes no separate clear.

Also from the same review: `doc/basics/authentication.md` still said login
"navigates to `MagicStarterConfig.homeRoute()`", which has been only the
fallback since this branch landed `navigateHome()`.

Test verified to fail without the clear.
@anilcancakir anilcancakir changed the title Survive the login bounce with an intended url, and follow magic_notifications to ^0.3.0 Survive the login bounce with an intended url Sep 9, 2026
@kodizm

kodizm Bot commented Sep 9, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of the one commit since ef36db9: it closes the logout-leak Minor I raised on the two explicit sign-out paths, and the ordering it relies on does hold - but the same leak is still reachable through the passive 401 logout, which nothing clears.

Scope: doc/basics/authentication.md, both controllers, and the auth controller test. The Sibling working trees explanation you gave earlier still matches what I see and needs no change here.

Major

lib/src/http/controllers/magic_starter_auth_controller.dart:361 — the fix covers the two logouts a user asks for, but not the one the app performs on its own. magic's AuthInterceptor calls Auth.logout() when a token refresh fails (auth_interceptor.dart:77), and AuthServiceProvider._registerAuthInterceptor() installs that interceptor unconditionally (auth_service_provider.dart:85), so it is live in every starter app. That flip re-runs the redirects the same way, EnsureAuthenticated records the protected route, and nothing pulls it - so a session that expires on /teams/settings still sends the next person who signs in on that device straight to it. Before this PR the value was never recorded at all, so this path is a regression the commit introduces rather than a pre-existing one. Clearing on Auth.stateNotifier dropping to signed-out, rather than at each call site, would cover all three. (correctness)

Minor

lib/src/http/controllers/magic_starter_profile_controller.dart:197 — the identical line in doDeleteAccount has no test; only the auth controller got one. CLAUDE.md's TDD rules ask for a failing test first, and deleting that line leaves the suite green.

test/http/controllers/magic_starter_auth_controller_test.dart:590 — the test seeds the intent with setIntendedUrl('/teams/settings') directly, so it proves the line runs but not the ordering the code comment asserts, namely that the redirect re-run lands inside await Auth.logout(). It would pass unchanged if the record arrived after the pull. For what it is worth the ordering does hold today: BaseGuard.logout bumps stateNotifier before returning (base_guard.dart:293), GoRouteInformationProvider wires notifyListeners straight to it (information_provider.dart:98), and go_router's parser returns a SynchronousFuture on the non-async redirect path (parser.dart:143). That chain is third-party internals, and a host route with an async redirect would break it, which is the argument for a real clear rather than a read-and-discard.

CHANGELOG.md:8 — the alpha.27 entry describes the intended-url feature but not that a sign-out now discards the intent, which is a user-visible rule about who a recorded destination belongs to. CLAUDE.md's post-change checklist asks for a changelog entry; doc/ was synced in this commit, the changelog was not.

Still open from my earlier review

_isGuestRoute (ensure_authenticated.dart:63) is still unreachable through the shipped route table, since auth_routes.dart:19 registers the whole auth group under middleware: ['guest']. Unanswered, still not blocking. The doc/basics/authentication.md Minor is fixed by this commit, and the PR title/description no longer claim the pin bump.

Tests

+85 across the two controller tests, navigates_routes_test.dart and ensure_authenticated_test.dart. The new sign-out clear is covered for the auth controller only; the profile controller's copy and the 401 path are not.

Checks I ran

  • flutter pub get → resolves cleanly against published packages (magic 0.0.9, magic_notifications 0.2.0).
  • flutter analyzeNo issues found! (ran in 18.0s).
  • flutter test on the four affected test files → All tests passed! (85 cases).
  • Read magic 0.0.9's base_guard.dart, auth_interceptor.dart, auth_service_provider.dart, magic_router.dart and go_router 17.5.0's information_provider.dart / parser.dart to check the notify-to-redirect ordering.
  • Full flutter test not run; the other ~106 test files are unverified by me.

…logout call site

Second review pass, and the reviewer was right that my first fix was the wrong
shape. Clearing after each `Auth.logout()` covered the two logouts a user asks
for and missed the one that matters most: magic's `AuthInterceptor` calls
`Auth.logout()` itself when a token refresh fails (`auth_interceptor.dart:77`)
and `AuthServiceProvider` installs that interceptor unconditionally, so a
session that simply EXPIRES on a protected route still recorded it. No call site
can reach that one, and it is a regression this branch introduced: before it,
the value was never recorded at all.

`SessionScopeSync` already listens to `Auth.stateNotifier`, which is the single
funnel all three pass through, and already has the branch for the identity
dropping to null. The clear goes there and the two call-site clears come out.

Deferred by a microtask, which is load-bearing rather than caution. The notifier
fans out in registration order: this listener attaches during the starter
provider's boot, go_router's attaches when magic builds the router AFTER boot,
so a synchronous clear runs BEFORE the redirect that records the value and
clears nothing. Named limit, carried into the changelog: a host route with an
async `redirect` records after the microtask and is not covered.

Three tests. Two fail against the previous commit (a session ending, and a
logout no call site performed). The third pins the opposite direction, that an
intent survives the login it was recorded for, and it takes TWO guards failing
together to break, so it survives removing either alone; verified against a
mutant that drops both. Its comment says so rather than claiming more than it
measures.

The changelog gained the rule itself, which the previous commit synced into
`doc/` and not there.
@kodizm

kodizm Bot commented Sep 9, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of the one commit since 0ba0a74: moving the clear onto the auth notifier is the right shape and does cover the passive 401 path I raised - but it hangs the clear off SessionScopeSync.attach(), which is opt-in and which nothing in this package calls, so an app that has not opted into session scoping now has no clear at all.

Scope: session_scope_sync.dart, both controllers, the two test files and CHANGELOG.md.

Major

lib/src/http/session_scope_sync.dart:182_forgetIntendedUrl only ever runs through the listener that attach() registers, and attach() is the host's call: doc/basics/session-scope.md:111 says "Attach it once, from your app's service provider boot()", and nothing in lib/src/providers/, install.yaml or assets/stubs/ calls or scaffolds it (grep for SessionScopeSync across lib/src/providers/ lib/src/cli/ assets/ install.yaml returns nothing). The previous commit's clears were unconditional at the two Auth.logout() call sites; both were deleted here. So for any starter app that has not adopted SessionScopedController, a sign-out on /teams/settings is back to sending the next person who signs in on that device straight there - the leak this commit's own doc comment describes. grep -rn "pullIntendedUrl" lib/ shows exactly two live call sites now: navigates_routes.dart:42 and this one. Either the starter's own provider should attach (or clear separately), or the precondition has to be stated. (correctness)

Minor

test/http/session_scoped_controller_test.dart:288 — "forgets the intended url on a logout NO call site performed" drives the same await Auth.logout() as the test directly above it, so it is a duplicate rather than coverage of the interceptor path; the comment concedes it ("Driven through the facade directly"). Deleting either test leaves the other green, and neither would catch a regression specific to the 401 route. Nothing covers the case an adopter actually hits - a host that never calls attach().

doc/basics/authentication.md:55 — "A sign-out clears any intent first" is now conditional on attach() having been called, and reads as an unconditional guarantee. doc/basics/session-scope.md also still describes SessionScopeSync as resetting controllers only, with no mention of its new intended-url responsibility; CLAUDE.md's post-change checklist asks for doc/ to be synced. The CHANGELOG.md:10 entry has the same gap - it names the notifier as "the one funnel all three logouts pass through" without saying the listener is opt-in.

Settled / still open

The microtask deferral is sound and does not depend on listener order as the comment implies: the whole record path (stateNotifierGoRouteInformationProvider.notifyListeners → parse → redirect) is synchronous, so the microtask runs after it whichever order the listeners attached in. The Auth.check() re-check inside the microtask plus the transition-to-null branch do both need to hold for the login case, as the new test asserts. _isGuestRoute (ensure_authenticated.dart:63) remains unreachable through the shipped route table - unanswered, still not blocking. The Sibling working trees red check still needs no change here.

Tests

+97 across the four affected controller/scope test files, all green. The auth-controller test for the old call-site clear was removed and replaced by the two SessionScopeSync tests; the profile controller's deleted line has no replacement test, but it is now covered by the same listener.

Checks I ran

  • flutter analyzeNo issues found! (ran in 16.8s).
  • flutter test test/http/session_scoped_controller_test.dart test/http/controllers/magic_starter_auth_controller_test.dart test/http/controllers/magic_starter_profile_controller_test.dart test/middleware/ensure_authenticated_test.dart+97: All tests passed!.
  • grep -rn "attach\|SessionScopeSync" lib/src/providers/ lib/src/cli/ assets/ install.yaml → no output; grep -rn "pullIntendedUrl\|setIntendedUrl" lib/ → the two live call sites above.
  • Full flutter test not run; the other ~106 test files are unverified by me.

Third review pass on the same defect, and the reviewer was right twice running.
The clear moved onto the auth notifier, which was the right funnel, but it went
into `SessionScopeSync` and that class is OPT-IN: `attach()` is the host's call
(`doc/basics/session-scope.md:111`) and nothing in `lib/src/providers/`,
`install.yaml` or `assets/stubs/` calls or scaffolds it. Verified by grep. So an
app that never adopted `SessionScopedController` lost the clear entirely, and
the previous commit had deleted the two unconditional call-site clears on the
way past. Strictly worse than either shape before it.

It now hangs off `MagicStarterServiceProvider.boot()`, which every starter app
runs. Same notifier, same microtask deferral, same `Auth.check()` re-check, and
the docblock says why it is not in `SessionScopeSync` so the next person does
not move it back.

The tests moved with it, and this is the point rather than bookkeeping: they
lived in the session-scope suite, which calls `attach()` itself, so they proved
the clear worked in exactly the configuration an adopter might not have. They
now boot the real provider, which is what an adopter has. Verified to fail
against a provider without the listener.

The duplicate the reviewer flagged is gone too. "A logout no call site
performed" drove the same `Auth.logout()` as the test above it, so it was a
second copy rather than coverage of the interceptor path; one test on the
notifier covers every route into it, which is the whole argument for putting the
clear there.

`doc/basics/authentication.md` and the changelog both said `SessionScopeSync`
clears it and read as unconditional guarantees. Both now name the provider and
say no host wiring is needed.
@kodizm

kodizm Bot commented Sep 9, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of the one commit since af6e0ea: moving the clear into MagicStarterServiceProvider is the right home and closes the opt-in SessionScopeSync gap I raised - but the static latch that guards the subscription only ever lets the FIRST boot in a process attach, which makes the new test order-dependent: it fails outright under --test-randomize-ordering-seed=1.

Scope: magic_starter_service_provider.dart, session_scope_sync.dart, both docs, and the two test files.

Major

lib/src/providers/magic_starter_service_provider.dart:104if (_authState != null) return; latches on a static that nothing ever clears, while Auth.stateNotifier resolves through the container and hands back a DIFFERENT notifier whenever the guard is re-bound (magic 0.0.9, auth.dart:194guard().stateNotifier; each Auth.fake() builds a fresh FakeAuthManager with its own ValueNotifier). So a second boot after a re-bind leaves the listener on the dead notifier and there is no clear at all. SessionScopeSync solves exactly this with detach(), and its own doc says why ("without it the listener outlives the guard it was registered against"); this copy has no equivalent. Either compare against the current notifier and move the subscription, or expose a detach. Evidence: flutter test test/middleware/ensure_authenticated_test.dart --test-randomize-ordering-seed=1is discarded when the session ends [E] Expected: false / Actual: <true> at ensure_authenticated_test.dart:127; seeds 2, 3 and 12345 pass, and the default (declaration) order passes. (correctness)

Minor

test/middleware/ensure_authenticated_test.dart:130 — a direct consequence of the above: because the discard test boots first and latches, survives the login it was recorded for boots a provider that early-returns, so its listener is on the previous test's notifier and the assertion holds vacuously. Its comment claims it "survives removing either one alone" of the two guards and was "verified against a mutant"; as ordered today it exercises neither guard. Fixing the latch restores the coverage.

lib/src/providers/magic_starter_service_provider.dart:70 — "Doubles as the attached flag, so a hot restart cannot double-subscribe" is doing no work: a Flutter hot restart re-runs main with statics reset, so the field is null again anyway. The case it actually needs to survive is the re-bind above, which it does not.

Still open from my earlier reviews

_isGuestRoute (ensure_authenticated.dart:63) remains unreachable through the shipped route table (auth_routes.dart:19 registers the whole group under middleware: ['guest']) - unanswered, still not blocking. The Sibling working trees red check still needs no change here.

Settled

The move off SessionScopeSync.attach() resolves the Major from my last review: the clear no longer depends on a host opting into session scoping, and the deleted _forgetIntendedUrl left no dangling references. CHANGELOG.md and doc/basics/authentication.md were both updated to describe the provider-hosted clear, so the post-change checklist in CLAUDE.md is satisfied.

Tests

+21 across the two touched test files in declaration order; the three SessionScopeSync intended-url tests were correctly removed and two replacements added at the provider level. One of the two is order-dependent and the other is currently vacuous, as above.

Checks I ran

  • flutter pub get → resolves cleanly (magic 0.0.9, magic_notifications 0.2.0).
  • flutter analyzeNo issues found! (ran in 15.1s).
  • flutter test test/middleware/ensure_authenticated_test.dart test/http/session_scoped_controller_test.dart+21: All tests passed! in default order.
  • Same file with --test-randomize-ordering-seed=1 → 1 failure, quoted above; seeds 2/3/12345 green.
  • Read magic 0.0.9's auth.dart, fake_auth_manager.dart and base_guard.dart to confirm the notifier is per-guard and re-bound per fake.
  • Full flutter test not run; the other ~106 test files are unverified by me.

Third finding on the same clear, and the reviewer proved it by running the
suite rather than reading it: `flutter test test/middleware/ensure_authenticated_test.dart
--test-randomize-ordering-seed=1` failed, and declaration order passed. I
reproduced both.

`if (_authState != null) return;` was a one-way latch over a static nothing
clears, while `Auth.stateNotifier` resolves through the container and hands
back a DIFFERENT notifier every time the guard is re-bound. So the first boot
in a process subscribed and every later one returned early, leaving the
listener on a notifier nobody bumps: no clear at all. `SessionScopeSync` solves
exactly this with `detach()` and says why in its own docblock; I copied the
field and not the escape.

It now compares the held notifier against the current one by identity and MOVES
the subscription. That needs the listener to be a named static rather than a
closure, since `removeListener` matches by identity and a fresh closure never
equals the one that was added.

The knock-on the reviewer also caught: with the latch, the second test booted a
provider that early-returned, so its listener sat on the previous test's
notifier and the assertion held vacuously. Its comment claimed a mutation
verification that the ordering had quietly disabled. Both are real again, and
verified: five seeds (1, 2, 3, 12345, 777) all pass, and a mutant with both
`Auth.check()` guards removed fails the survives-login test under seed 1.

Dropped the "a hot restart cannot double-subscribe" line while I was there. A
hot restart re-runs main with statics reset, so the field is null again anyway;
the case it has to survive is the re-bind, which is the one it was not
surviving.
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@anilcancakir

Copy link
Copy Markdown
Contributor Author

@kodizm

@kodizm

kodizm Bot commented Sep 10, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of the one commit since 7347f45: the identity compare plus the named static listener fixes the latch I raised, the order-dependent failure is gone across every seed I tried, and I have no new findings.

Scope: lib/src/providers/magic_starter_service_provider.dart only (30 insertions, 14 deletions). The subscription now moves rather than latching, and hoisting the closure to static void _forgetIntendedUrl() is what makes removeListener able to match it, which is the right reason for the shape change. _authState?.removeListener(...) against an already-disposed notifier is safe: ChangeNotifier.removeListener has no dispose assert, and the suite exercises it every time Auth.fake() re-binds.

The knock-on Minor is fixed with it: survives the login it was recorded for now boots against its own notifier instead of inheriting the previous test's, so both Auth.check() guards are actually exercised rather than holding vacuously.

Still open from my earlier reviews

_isGuestRoute (ensure_authenticated.dart:63) is still unreachable through the shipped route table, since auth_routes.dart:19 registers the whole auth group under middleware: ['guest']. Raised three reviews ago, unanswered, still not blocking. The Sibling working trees red check still needs no change here - the stamp test skips when no sibling checkout exists, and your explanation (magic's default branch stamped alpha.23 until fluttersdk/magic#152 merges) matches what I see.

Tests

+24 across ensure_authenticated_test.dart, session_scoped_controller_test.dart and navigates_routes_test.dart, green under declaration order and under seeds 1, 2 and 777. Seed 1 is the one that failed on the previous commit, so the fix is confirmed by the same command that found the bug. The five controllers now routing through navigateHome() still have no test on that wiring - unchanged, and still not something I would block on.

Checks I ran

  • flutter analyzeNo issues found! (ran in 15.0s).
  • flutter test test/middleware/ensure_authenticated_test.dart test/http/session_scoped_controller_test.dart test/http/controllers/concerns/navigates_routes_test.dart --test-randomize-ordering-seed={1,2,777}+24: All tests passed! on all three seeds.
  • Read the full post-change _forgetIntendedUrlOnSignOut / _forgetIntendedUrl pair rather than the diff hunk alone.
  • Full flutter test not run - it exceeds my time budget, so the other ~106 test files are unverified by me.

Raised in three consecutive reviews and never answered, which is my omission
rather than the reviewer's persistence. The branch IS unreachable through this
package's own routes: `auth_routes.dart:19` registers the whole auth group
under `middleware: ['guest']`, so `EnsureAuthenticated` never sees
`/auth/register` or its siblings. Verified.

It stays because this middleware is public API and a host applying `auth`
globally over a shell route is a supported configuration, not a hypothetical.
Without the branch such an app bounces a visitor off `/auth/register`, records
it, and sends them back to a guest-only route after they sign in.

Dead for us, live for an adopter, and the two tests over it call
`redirectTarget` directly so nothing shows the router reaching it. All of that
now sits in the docblock instead of in a review thread.
@anilcancakir

Copy link
Copy Markdown
Contributor Author

@kodizm

@anilcancakir
anilcancakir merged commit 1931ddf into main Sep 10, 2026
5 checks passed
@anilcancakir
anilcancakir deleted the feature/intended-url-after-login branch September 10, 2026 01:26
@kodizm

kodizm Bot commented Sep 10, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental review of the one commit since 7347f45 that I had not already read: 9a72bce is comment-only, and it answers the _isGuestRoute question I had left open, so I have no findings.

Scope: lib/src/middleware/ensure_authenticated.dart, +15 lines, all docblock. The other file in the incremental range, magic_starter_service_provider.dart (f1cd745), is the commit I reviewed at 00:50 and it is unchanged since.

Settled

_isGuestRoute — you have answered the finding: the branch is dead through this package's own route table but live for a host applying auth globally over a shell route, and the middleware being public API makes that a supported configuration. That is a deliberate-and-explained answer, so I am dropping it, and the reasoning now sits where the next reader will find it rather than in this thread.

Tests

No test change, correctly - nothing executable changed. Coverage is as I described last review: +24 across ensure_authenticated_test.dart, session_scoped_controller_test.dart and navigates_routes_test.dart, green under declaration order and seeds 1/2/777. The five controllers now routing through navigateHome() still have no test on that wiring; unchanged, and still not something I would block on.

Checks I ran

  • flutter analyzeNo issues found! (ran in 14.6s).
  • Read the full post-change docblock rather than the hunk alone, and confirmed via git log --stat that 9a72bce touches only this one file.
  • No tests re-run: the only change since my last review is a comment, and the suite I ran at 00:50 covers the same bytes of executable code.
  • Full flutter test still not run; the other ~106 test files are unverified by me.

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