feat(auth): finish the email-change flow — verify, undo and a pending-change banner (#72) - #108
Merged
Merged
Conversation
Plumb the two endpoints that finish an email change and were previously web-only, so an Android user can start and finish the change in the app: - POST /api/auth/verify-email-change — confirm the new address - POST /api/auth/undo-email-change — revert a change the user did not make Both are unauthenticated in the OpenAPI spec (x-auth-type: none, empty security), so the repository submits the token without requiring a session — the point of the undo link is that whoever reaches for it may already be locked out. Deep links follow the app's existing NotificationLaunch pattern: a pure EmailChangeLink parser maps the emailed URL onto an in-app route, MainActivity resolves the VIEW intent through AuthRoutes.routeForEmailChangeLink, and the nav host navigates there whether or not the app started signed in. Manifest intent filters cover both https://interlinedlist.com/{verify,undo}-email-change and the interlinedlist:// equivalents. Malformed links (no token, blank token, foreign host, plain /verify-email) resolve to nothing and the screen reports an invalid link rather than submitting or reporting success. Account settings gains a pending-change banner driven by the live pendingEmail field on GET /api/user: it shows the address awaiting confirmation, offers a resend, and clears once the server reports the change confirmed or undone. The API exposes no endpoint that cancels a pending change (DELETE on /api/user/change-email/request is 405 and no /cancel route exists), so the banner states plainly that the emailed undo link is how to stop it rather than shipping a button that does nothing. Tests: deep-link parsing for all four link shapes plus the malformed cases; verify/undo ViewModel paths including the server's own error message; the repository request shapes over MockWebServer; the pending state rendering, clearing and resend on the account ViewModel; Compose coverage for the result screen copy and the pending banner. Closes #72
Member
Author
|
Merged into |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #72. Part of epic #71.
What changed
POST /api/auth/verify-email-changeandPOST /api/auth/undo-email-changeare now wired (bodies{ "token": … }), going through the existingsafeApiCall→AppErrormapping so 400/409/429surface the server's own message. An Android user no longer has to finish an email change on the web.
Auth requirement was checked, not assumed. Both operations are
x-auth-type: none,x-subscription-tier: public,"security": []in the spec — so neither needs a session, and theundo path deliberately works signed-out, which matters because undo exists for the case where
the change was not the user's doing. There is a repository test asserting the call succeeds with the
session cleared.
Deep links
EmailChangeLink.parse()is pure JVM (noandroid.net.Uri) so it is unit-testable. It maps thefour link shapes onto
VERIFY/UNDO+ token, toleratingwww., a trailing slash, extra queryparams, fragments and percent-encoding — and returns
nullfor a missing/blank token, a look-alikehost, the unrelated
/verify-emaillink, or garbage.It follows the app's existing
NotificationLaunchpattern rather thannavDeepLink:MainActivityresolvesintent.dataStringthroughAuthRoutes.routeForEmailChangeLink(...). Onemechanism, no double navigation, deterministic whether the app starts signed in or out, and covered
by plain unit tests.
Also fixed in passing:
AuthRoutes.RESET_DEEP_LINKS/VERIFY_DEEP_LINKSare nowby lazy—NavDeepLinkparses its pattern withandroid.net.Uri, which would blow up any JVM test that merelytouched the
AuthRoutesobject.Pending-change state
pendingEmail(confirmed present on the liveGET /api/user,nullwhen nothing is in flight) isread on entry and after a request/resend, and rendered as a banner with the address and a Resend
confirmation email action. Deliberately not cached in Room — it sits with the other "always
fresh" account surfaces and must never be shown stale, since it clears from a link tapped elsewhere.
Two things to confirm / decide
A. The emailed link URLs — high confidence, but please check the mail templates. They are not
documented, so I derived them live:
GET /verify-email-change?token=probeGET /undo-email-change?token=probe/auth/verify-email-change,/email-change/verify,/account/verify-email-change,/change-emailSo the paths match the existing
/verify-emailand/reset-passwordconvention andtokenis theparameter name (it is also the field name in both API request bodies). Parsing is defensive around
tokenregardless, and both custom-scheme shapes are accepted, so a template usinginterlinedlist://would also work.B. There is no "cancel pending change" endpoint, so I did not ship a fake button. The issue asked
for cancel; the API does not have it. Probed:
DELETE /api/user/change-email/request→ 405 (routeexists, POST only);
POST /api/user/change-email/cancel→ 404;DELETE /api/user/change-email→ 404;PATCH /api/user/updatehas nopendingEmail/emailproperty.The product's actual safety valve is
undo-email-change, which needs a token from the email. So thebanner says plainly: "Your account keeps its current email until you open the link we sent to this
address. To stop the change, use the 'undo' link in the email sent to your current address." If a
cancel endpoint is added later,
resendEmailChangeis the shape to copy.Pre-existing discrepancy, left alone
The help page documents
POST /api/user/change-email/requestas{ newEmail, password }, but theOpenAPI spec and the already-shipped
ChangeEmailRequestsend onlynewEmail. Resend reuses thatexisting call, so if the server really requires
password, resend fails exactly the way the initialrequest already would. Out of scope here, but worth verifying.
Verification
./gradlew :app:assembleDebug testDebugUnitTest --rerun-tasks→ BUILD SUCCESSFUL, 941 tests, 0failures. New:
EmailChangeLinkTest(14),EmailChangeViewModelTest(6),AccountSettingsViewModelTest(12, up from 5), plus 5 inDefaultAuthRepositoryTestand 2 inDefaultProfileRepositoryTest. Compose tests compile (assembleDebugAndroidTestgreen for:feature:auth,:feature:profile,:app) but were not executed — no emulator.