Repository navigation
feat(settings): the Integrations surface — and three live identity defects underneath it (G33) - #93
Merged
Merged
Conversation
…underneath it
The Linked accounts pane could only link. Once an account was connected there was
no way to check it, reconfigure it, or remove it from the Mac. Probing that
surface turned up three live defects, and they matter more than the pane.
Mastodon arrives instance-qualified. The wire sends
`"mastodon:techhub.social"`, and IdentityProvider matched `"mastodon"` exactly —
so every Mastodon identity fell to `.other` and rendered as an unknown provider
with none of its actions, while the instance host was discarded. The doc comment
claimed the host was "carried on the LinkedIdentity"; there was no such field.
It is split out now, and `providerWireToken` reassembles it for unlink and
verify — a bare "mastodon" on an account with two instances is ambiguous enough
that the server could disconnect the wrong one.
X was missing from the provider enum. The consequence is sharper than a missing
row: a linked X identity decoded as `.other("twitter")`, so the one provider the
account could already cross-post to was the one the client could not recognise.
`GET /api/user/identities` accepts Bearer, contrary to its `.session`
annotation. Not a hard failure — the kit lazily establishes a cookie session —
but a redundant credentialed round-trip on every load, and the same class of
error as the send-verification-email correction in PR #83, in the other
direction.
The pane itself is data-driven rather than a switch statement per provider:
supportsMultipleInstances, requiresInstanceHost, isVerifiable and
isDisconnectable live on the provider, so a row is a projection of the model
instead of a parallel copy of the same rules. That is also what makes Mastodon
work — it is the one provider with several connections, and it renders one row
per instance.
Two smaller judgement calls. Disconnect is not optimistic: it stops cross-posting,
and a row that vanishes and returns because the write failed is worse than one
that waits. And a failed verify reports the request error rather than "not
responding" — a failed ask is not a failed connection, and saying otherwise
would be a lie about the provider.
Four pieces are deferred on evidence, not effort: LinkedIn posting-target editing
(the read is empty on this account, and it is #79's supply side), the
githubDefaultRepo write and Reconnect for Issues (no accessible repos — see #51),
and the reconnect-reminders toggle (its backing field is on neither /api/user nor
the notification-preferences catalogue, so nothing is wired to a guess).
Refs #47
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gSWb3scYobtxLJioV1qF9
This was referenced Sep 15, 2026
Closed
Open
Adron
added a commit
that referenced
this pull request
Sep 17, 2026
Four files conflicted. Three were textual overlaps where both sides add distinct members; the fourth needed more than a mechanical resolution. AppTests/Support/StubUserService.swift was the only file git could not merge itself. Both sides add members to the same type — this branch's profile-settings stubs and #93's identity stubs — so the resolution is to keep both. Rebuilt from dev's version with this branch's four additions reapplied, rather than by concatenating the conflict hunks: the hunk boundaries crossed a closing brace, so a naive keep-both dropped the `}` of `enqueueRequestPasswordReset(failure:)` and nested the rest of the type inside it. It compiled far enough to look plausible and failed with "Attribute 'private' can only be used in a non-local scope" 300 lines later. UserDTO.swift, UserService.swift and SettingsRootView.swift auto-merged, and the first two are genuinely fine — both sides only added. SettingsRootView's auto-merge was textually clean and semantically wrong, which is the interesting part of this merge. PR #94 introduced `SettingsTab` and converted the pane into a `TabView(selection:)`, tagging every tab it knew about. It was written against a dev that predated both the Integrations tab (#93) and the Profile tab (this branch), so neither got a tag. Git had no reason to flag that — no line conflicts — but under `TabView(selection:)` an untagged tab cannot be selected programmatically at all. Both are tagged here, and the enum gains the two cases. The same gap had a second consequence: the sidebar's Integrations row set `settingsTab = .linkedAccounts`, which was the honest best available when no Integrations tab existed and is simply wrong now that one does. A row labelled Integrations opening Linked accounts is the kind of thing that survives review because it compiles. Retargeted. The tab-count assertion becomes a full set. A count was already there and did not catch this — the count was right, the tabs were unreachable — so it now pins the identities, which at least fails loudly when a pane arrives without one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 17, 2026
Merged
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.
Summary
Addresses #47 (G33). Ships the Integrations pane and — more importantly — three live defects the probe found underneath it. The remaining four pieces are deferred on evidence, not effort, and each is named below.
This issue is sized L and anticipated a split. The probe changed which half is urgent: Mastodon turned out to be a defect, not a modelling preference, so it leads.
The probe
GET /api/user/identities, live, 2026-09-15:{"identities":[ {"provider":"bluesky","providerUsername":"interlinedlist.bsky.social", …}, {"provider":"mastodon:techhub.social","providerUsername":"interlinedlist_crew@techhub.social", …}, {"provider":"twitter","providerUsername":"interlinedlist", …}, {"provider":"github","providerUsername":"InterlinedListMessenger", …}]}1. Every Mastodon identity rendered as an unknown provider
IdentityProvider.init(wireToken:)matched"mastodon"exactly, so"mastodon:techhub.social"fell to.other— an unknown provider, with none of its actions offered — and the instance host was discarded entirely.The doc comment claimed the host was "carried on the
LinkedIdentity". There was no such field.LinkedIdentity.instancenow carries it, andproviderWireTokenreassembles it — because a bare"mastodon"on an account with two instances is ambiguous enough that the server could disconnect the wrong one. There is a test for exactly that.2. X was missing from the provider enum
The issue flagged this; the consequence is sharper than a missing row. A linked X identity decoded as
.other("twitter")— so the one provider the account could already cross-post to was the one the client could not recognise.3.
GET /api/user/identitiesaccepts BearerContrary to its
.sessionannotation. Not a hard failure — the kit lazily establishes a cookie session — but a redundant credentialed round-trip on every load of the pane. Same class of error as thesend-verification-emailcorrection in PR #83, in the opposite direction.The pane
One row per connection, with Verify and Disconnect, per-row status, and per-row errors.
It is data-driven rather than a switch statement per provider — what a row offers comes from
supportsMultipleInstances/requiresInstanceHost/isVerifiable/isDisconnectableon the provider itself. That is what the issue asked for, and it is what makes Mastodon work: it is the one provider with several connections, so it renders one row per instance and stays in the "connect an account" list when the others disappear.The disconnect confirmation names what stops working, per provider — losing GitHub takes GitHub-backed lists with it; losing a cross-post provider silently stops those posts. A generic "are you sure" tells the user nothing they did not already know.
Two smaller calls:
Deferred, and why
GET /api/linkedin/posting-targets→{"targets":[],"orgScopeMissing":false}. No populated payload has been seen. Building an editor against an empty array is how the G21 and G25 silent-decode defects happened. It is also #79's supply side. (orgScopeMissingis an undocumented field this issue did not mention.)githubDefaultRepowrite · Reconnect for GitHub IssuesGET /api/github/repos→[]. No accessible repositories, exactly as #51 records, so a repo picker cannot be exercised end to end. The scope-upgrade path shares the OAuth flow with #51.GET /api/usernor/api/user/notification-preferences(an event catalogue withpush/inAppchannels and no reconnect key). Backing field unidentified — nothing is wired to a guess.ASWebAuthenticationSessionand the Mastodon instance prompt. Two copies of an OAuth handshake is worse than two panes.Verification
xcodebuild build→** BUILD SUCCEEDED **xcodebuild test(App) →Executed 979 tests, with 0 failures·** TEST SUCCEEDED **(was 968)swift test InterlinedDomain→Executed 1021 tests, with 0 failures(was 1012)swift test InterlinedKit --skip ContractTests→Executed 480 tests, with 0 failures(was 477) — liveContractTestsskipped because the account is rate-limited from this session's recon; unmodified by this PR.swift test InterlinedPersistence→Executed 140 tests, with 0 failuresNew tests:
IdentityProviderTokenTests(10, against the captured identity list),IntegrationsViewModelTests(11), plus corrected/extendedUserEndpointTests.Two existing tests changed, both deliberately:
linkableProvidersnow includes X, and the identities builder now asserts.bearer. Both are the point of the change, not collateral.🤖 Generated with Claude Code
https://claude.ai/code/session_016gSWb3scYobtxLJioV1qF9