feat(lists): email invites — send, list and revoke (#58) - #119
Merged
Merged
Conversation
Adds the email-invite flow to the list access (watchers) screen, alongside the existing per-person watcher roles rather than replacing them, mirroring the document flow landed in #59: - POST/GET /api/lists/{id}/invites and DELETE .../invites/{token} plumbed through ListsRepository as sendInvite/getInvites/revokeInvite. - An "Invite by email" form (address + Read-only/Edit/Admin role) and a "Pending invites" list showing role, derived status (Pending / Accepted / Expired / Revoked) and expiry, each row revocable. - Sending is gated on CustomerStatus.isSubscriber via the shared GET /api/user check, so a free account issues no write at all; listing and revoking are never gated, matching the server — a lapsed owner can always shut off access they previously granted. - Addresses are validated client-side before any request, and server rejections are surfaced with the server's own message instead of a generic failure. Invite roles use the server's sharing vocabulary (watcher / collaborator / manager) behind this module's Read-only / Edit / Admin labels, and the create response's missing token is recovered from the returned landing URL so a just-sent invite can be revoked. Re-inviting an address replaces the existing row, because the server treats it as idempotent rather than an error. Accepting an invite (/api/lists/invite/{token}) is deliberately out of scope; it is covered by #60 for lists and documents together. Closes #58 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 #58. Part of epic #57. Mirrors #59 (documents).
What changed
Email invites for lists, on the list's access screen (
WatchersScreen— the per-person accesssurface, the list counterpart of the document collaborators sheet), as a nested
state.invitesobject.
sendInvite, so no caller can bypass them. Afree account issues no write at all (one
/api/userread, then a localSubscriptionRequired);an invalid address issues no request whatsoever.
revokeInvitemakes exactly oneDELETEand no subscription lookup,asserted with
server.requestCount == 1.token, so it is recovered from the landing URL's last path segment —otherwise a just-sent invite could not be revoked.
revokedAt→accepted→ expiry vs. clock → pending); there is noserver
statusfield.the server's idempotent re-issue.
Accepting an invite is not implemented — that is #60, which covers lists and documents together.
Verification
./gradlew :app:assembleDebug testDebugUnitTest→ BUILD SUCCESSFUL, 1298 tests, 0 failures (39new). 6 Compose tests compile, not executed (no emulator).
Four things worth your attention
currentUserIsSubscriber()treatsCustomerStatus.UNKNOWNas unreadable (fail open) rather than as not a subscriber. Anunrecognised
customerStatusstring — say a futuresubscriber:lifetime— maps toUNKNOWN,whose
isSubscriberisfalse, which would lock a paying owner out of their own invite formwithout ever asking the server. Covered by a test.
:feature:documentshas the same latentbug and needs the same one-line fix; filed separately rather than touched here.
(this issue's wording); documents label the same API values Viewer / Editor / Admin (Invites: send and revoke document invites #59's).
The help centre's own table says Viewer/Editor/Admin. That is a product decision — say which you
want and it is a one-line change in
InviteLabels.InviteStatus/InviteRole/InviteEmail/inviteExpiryLabelare now near-identical in both modules, per the self-contained-moduleconvention. If a consolidation pass ever happens,
InviteEmailandinviteExpiryLabelare thegenuinely generic pieces;
InviteRoleis not, precisely because of point 2.DefaultListsRepositorygaineduserApi: InterlinedListApias its second constructorparameter. Hilt provides it with no DI-module change, but a parallel branch constructing the
repository directly will conflict on those seven test
setUps.expiresAtis modelled end to end but the UI never sets it, so invites are always created with noexpiry; the "Expired" state is reachable only for invites created elsewhere.