feat(deep-links): open the web's canonical permalinks in-app (W7) - #71
Open
Adron wants to merge 1 commit into
Open
feat(deep-links): open the web's canonical permalinks in-app (W7)#71Adron wants to merge 1 commit into
Adron wants to merge 1 commit into
Conversation
Links copied from the web mostly did not open in the app, and the most
common one landed on the wrong screen: `/user/<u>/status/<id>` is the
web's canonical *message* permalink, but the parser read only the first
path segment and opened the author's profile instead of the post.
- AppDeepLink gains `.list(id:)`, `.publicList(owner:id:)` and
`.publicDocument(owner:id:)`; the `user` case now inspects the second
segment (`status` / `lists` / `documents`) before falling back to the
profile, and the `lists` case accepts a bare id alongside the
share-token form.
- `.list(id:)` routes to ListLinkView, which resolves the list from the
store's owned/shared collections before falling back to a new
`APIClient.list(id:)` (`GET /api/lists/:id`), then hands a full
UserList to ListDetailView.
- A 403/404 is normalised to `ListAccessError.noAccess` and rendered as
a plain "no access to this list" state. Both statuses arrive from the
backend *with* an `{error}` body, so the generic mapping flattened them
into `.forbidden`/`.server` and the opener could not tell "not yours"
apart from "server trouble"; a new `get(_:mappingStatuses:)` transport
helper catches the status before that mapping runs.
- Delete the stale claim that a bare `/lists/:id` cannot be routed for
want of an owner username, in the code and in the-gaps.md. It was
false: `GET /api/lists/:id` is Bearer-ready and authorizes by role
(owner/manager/collaborator/watcher).
- Tests: one per row of the issue's URL table, in both the custom-scheme
and https (bare + www) forms, plus foreign-host rejection and the
no-access path. 969 unit tests green.
The AASA already wildcards `/user/*`, `/message/*`, `/lists/*` and
`/documents/*`, so every new shape is covered for Universal Links.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jd9n499evEUh6A7hifcF3N
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
Links copied from the web mostly did not open in the app, and the most common one landed on the wrong screen:
/user/<u>/status/<id>is the web's canonical message permalink, butAppDeepLink.parseread only the first path segment and opened the author's profile instead of the post. A bare/lists/<id>— a URL iOS itself generates viaILWebURL.list(_:)— was explicitly rejected.Closes #61.
What's included
Every row of the issue's table now opens its own screen, in both the
interlinedlist://andhttps://interlinedlist.com(bare +www.) forms:/user/<u>/status/<id>.message→MessageLinkView)/user/<u>/lists/<id>.publicList→PublicListDetailView/user/<u>/documents/<id>.publicDocument→PublicDocumentReader/lists/<id>.list→ListLinkView→ListDetailView/message/<id>,/documents/<id>,/lists/shared/<t>,/documents/shared/<t>usercase inspects the second segment (status/lists/documents) before falling back to the profile; anything else under a profile is still the profile. Thelistscase accepts a bare id alongside the share-token form. Unknown targets and foreign hosts still returnnil.ListLinkViewresolves the list fromAppDataStore(owned, then shared-in) and only falls back to a newAPIClient.list(id:)(GET /api/lists/:id) when it isn't already known.ListAccessError.noAccessand renders "You don't have access to this list", never a crash or a silent no-op. Both statuses arrive from the backend with an{error}body, so the generic mapping flattened them into.forbidden/.serverand the opener could not tell "not yours" from "server trouble"; a newget(_:mappingStatuses:)transport helper catches the status before that mapping runs. A 401 still routes tohandleUnauthorized(), not to the no-access state./lists/:idcan't be routed for want of an owner username is gone from the code comment andthe-gaps.md(two live rows). It was false:GET /api/lists/:idis Bearer-ready and authorizes by role (owner / manager / collaborator / watcher).project.pbxproj(no synced groups):APIClient+Lists.swift,ListLinkView.swift,APIClientSingleListTests.swift.Testing
xcodebuild ... build— BUILD SUCCEEDEDxcodebuild ... -parallel-testing-enabled NO -skip-testing:InterlinedListTests/E2EReadOnlyTests test— 969 tests, 0 failureswww.), theILWebURL.list→.listround-trip, foreign-host rejection, and the 403/404/401/500 paths ofAPIClient.list(id:).Notes
/user/*,/message/*,/lists/*,/documents/*, and AASA*spans/, so every newly-routed shape is covered — no AASA or entitlement change needed. Inboundhttpsstill waits on the Associated Domains portal capability tracked inApp-Store-Deployment-Checklist.md(A2).?tokenquery items remain parse-only and are never logged.simctl openurlraises an "Open in InterlinedList?" system dialog and no UI-automation tap tool is enabled in this environment. Routing is covered by the unit tests, and every deep-link destination needs onlyauthState, which each new sheet case injects.🤖 Generated with Claude Code