feat(integrations): unlink, re-verify and connection health on connected accounts (#40) - #111
Merged
Merged
Conversation
The connected-accounts screen was read-only: DELETE /api/user/identities was
declared but unused and POST /api/user/identities/verify was never called, so a
connection could quietly lapse and take cross-posting with it.
Contracts (confirmed against the OpenAPI spec before wiring):
- DELETE /api/user/identities takes the identity's `provider` as a QUERY
PARAMETER (`?provider=mastodon%3Atechhub.social`), not a body or path segment.
- POST /api/user/identities/verify takes `{ "provider": "..." }` in a JSON BODY.
Both success bodies are unspecified, so they are read as raw ResponseBody and
discarded; the refreshed list is the source of truth.
getConnectedAccounts() now merges GET /api/user/identities into the per-provider
status rows, which supplies the unlink/verify key plus connectedAt and
lastVerifiedAt. One identity becomes one row, so Mastodon instances stay distinct
and unlinking one cannot take out another. A failed identities read degrades to
status-only rows rather than blanking the screen.
Connection health uses a 30-day staleness threshold: the help centre
(/help/cross-posting -> "Keeping your connections active") documents platform
authorizations expiring on their own, LinkedIn's "after a couple of months", so
30 days leaves roughly a month of warning before the earliest point a cross-post
could start failing. Three states render distinctly - verified recently, verified
too long ago ("Check connection"), and never verified - and the copy names the
consequence ("posts may stop reaching LinkedIn without an error") instead of
printing a raw timestamp.
Unlink is confirmed first, and the dialog states the consequence plainly: it
stops cross-posting to that network. Neither mutation is applied optimistically -
on success the list is re-read so the screen can never show a state that is no
longer true, and on failure the connection stays exactly where it was with the
server's own message surfaced.
Linking itself is untouched: it still needs a browser OAuth redirect (#39).
Tests: connection-health states and the 30-day boundary; the three rendering
cases; unlink round-trip with list refresh; failed unlink keeps the row and
surfaces the server message; verify round-trip with refresh; identity merge,
query-parameter and request-body shapes against MockWebServer; plus Compose
coverage of the confirmation dialog and the stale badge.
Closes #40
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 #40. Part of epic #38. (OAuth linking itself, #39, remains blocked on redirect URIs.)
Contracts checked before wiring
DELETE /api/user/identitiestakesprovideras a query parameter(
?provider=mastodon%3Atechhub.social) — not a body, not a path segment. Confirmed in the spec(
"name": "provider", "in": "query") and consistent with what:feature:profilealready does.POST /api/user/identities/verifytakes{ "provider": "…" }in a JSON body.type: object), so they are read as a rawResponseBodyanddiscarded — the refreshed list is the source of truth.
Envelope confirmed live (the agent could not reach the API; I checked afterwards):
GET /api/user/identities→{"identities":[{id, provider, providerUsername, profileUrl, avatarUrl, connectedAt, lastVerifiedAt}]}. So the{ "identities": [...] }modelling here isright, and the published help page's bare-array example is wrong. Also confirmed:
providerisinstance-qualified — a real row reads
"mastodon:techhub.social".Staleness threshold: 30 days
/help/cross-posting→ Keeping your connections active says platform authorizations expire ontheir own, and that LinkedIn's "generally expire after a couple of months and can't be refreshed
automatically". 30 days is half the shortest documented expiry — roughly a month of warning before
the earliest point a cross-post could start silently failing, while staying quiet for anyone who
verifies regularly.
The UI says what it means rather than printing a timestamp:
Two design points
getConnectedAccounts()now mergesGET /api/user/identitiesinto the existing per-provider/api/auth/<p>/statusrows. One identity = one row, so multiple Mastodon instances staydistinct — collapsing them into a single "Mastodon" row would have made unlink hit an arbitrary
instance. (The live account has
mastodon:techhub.social, so this is not hypothetical.) A failedidentities read degrades to status-only rows rather than blanking the screen.
message; only success triggers a reload.
The unlink confirmation is provider-aware: cross-post targets get "This stops cross-posting to
LinkedIn."; GitHub — not a cross-post target — gets honest wording instead of a claim that is not
true.
Verification
./gradlew :app:assembleDebug testDebugUnitTest→ BUILD SUCCESSFUL, 1069 tests repo-wide, 0failures.
:feature:integrationswent from 26 to 66 tests. 4 Compose tests compile, not executed(no emulator).
Mutation-checked, since everything passed first try: widening
STALE_AFTERto 90 days fails 5tests (the three-state and boundary assertions); adding a
reload()to the failure branch fails thetwo "failed unlink/verify leaves the row untouched" tests. Both reverted.
No
:appchange was needed —ConnectedAccountsRoute(onBack)'s signature is unchanged.Remaining risk
The verify response body is ignored. If the server can return 200 with something like
{"verified": false}rather than a 4xx, a failed verification would read as success until therefreshed
lastVerifiedAtcontradicts it. I did not callPOST /api/user/identities/verifylive tosettle this, since it mutates
lastVerifiedAton your real linked accounts.Also noted, out of scope:
:feature:profilehas its own connected-accounts screen with overlappingunlink logic. Deduplicating the two is a reasonable future issue.