Skip to content

fix(share): explicit consent for private→team vault conversion, and make the chosen role reach the invitee - #179

Merged
kipavy merged 2 commits into
devfrom
feat/vault-share-sheet
Aug 25, 2026
Merged

fix(share): explicit consent for private→team vault conversion, and make the chosen role reach the invitee#179
kipavy merged 2 commits into
devfrom
feat/vault-share-sheet

Conversation

@kipavy

@kipavy kipavy commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Closes part of #68.

What this does

Replaces the silent private→team vault conversion with an explicit consent moment, and fixes three real defects found while driving the running app against a live server.

The conversion is no longer a side effect

Adding one person to a private vault used to create a team, link the vault, initialise a vault key and convert the vault to cloud-synced — with no dialog and no explanation. Inviting someone now opens a modal that states the three costs before anything happens:

  • Opens only while Voltius can reach the sync server — no offline access.
  • Everyone you add gets a copy of its key. Removing them later doesn't take it back.
  • There's no way back to private.

PrivateVaultInvitePanel is deleted. A private vault now goes consent → convert → the existing team invite panel, which removes the second invite panel without changing the Members page layout.

Bugs fixed

The chosen role never reached the invitee. addMemberById creates a pending invitation, not a member row, so the follow-up assignMemberRole 404s — and it was swallowed by an empty catch. Every invitee silently landed on the default role. The role is now passed to addMemberById, which the server stores on the invitation; role assignment happens only for someone who is already a member. Reproduced against a live server before fixing:

POST /v1/teams/…/members              → 201  "Pending invitation created for existing user"
POST /v1/teams/…/members/<user>/roles → 404

Failed invites reported success. The invite handler caught all errors, then unconditionally cleared the typed query and told its parent an invite had happened. On failure the query now survives so the user can retry, and the named error toast is the only report.

Seats rendered ? available · ? total when the subscription load failed. There is now an explicit unavailable state.

Smaller fixes

  • Text on accent backgrounds used a hardcoded #08151b, which is unreadable under a dark accent. Now uses --t-on-accent, which is computed from accent luminance.
  • The vault header's member avatar stack was hover-only with cursor-default — unreachable by keyboard and on touch. It is now a real focusable button with aria-haspopup/aria-expanded and Enter/Space handling. The + affordance itself is unchanged.
  • markTeamVaultLoadedAfterLocalActivation was imported from the wrong module; the test mocked the whole path so it passed while production would have thrown.

Not in this PR

Verification

Full suite: 508 files, 3911 tests, exit 0. tsc --noEmit exit 0.

Driven end to end in the headless build against a throwaway server: consent → convert → invite, with the invitation's role confirmed in the database as the role chosen in the UI rather than the default.

…e chosen role reach the invitee

Adding one person to a private vault used to create a team, link the vault,
initialise a vault key and convert the vault to cloud-synced — with no dialog
and no explanation. Inviting someone now opens a modal stating the three costs
before anything happens: no offline access, members hold a copy of the vault
key that removal does not take back, and no way back to private.

PrivateVaultInvitePanel is deleted. A private vault goes consent → convert →
the existing team invite panel, which removes the second invite panel without
changing the Members page layout.

Bugs fixed, all found by driving the running app against a live server:

- The chosen role never reached the invitee. addMemberById creates a pending
  invitation, not a member row, so the follow-up assignMemberRole 404s — and an
  empty catch swallowed it, leaving every invitee on the default role. The role
  now travels on the invitation; role assignment runs only for an existing
  member.
- Failed invites reported success: the handler caught every error, then cleared
  the typed query and told its parent an invite had happened.
- Seats rendered "? available · ? total" when the subscription load failed.
- Text on accent backgrounds used a hardcoded #08151b, unreadable under a dark
  accent; now uses --t-on-accent, computed from accent luminance.
- The vault header's member stack was hover-only with cursor-default,
  unreachable by keyboard and on touch; it is now a focusable button with
  aria-haspopup/aria-expanded and Enter/Space. The + affordance is unchanged.
- markTeamVaultLoadedAfterLocalActivation was imported from the wrong module;
  the test mocked the whole path, so it passed while production would have
  thrown.

Refs #68
@kipavy
kipavy force-pushed the feat/vault-share-sheet branch from 50551b4 to 8392f60 Compare August 25, 2026 10:55
…e selected

Two live-verified defects in the invite panel:

- A transport failure (fetch/reqwest rejected before any HTTP status came
  back) propagated its raw message straight into the panel's error text,
  e.g. "error sending request for url (http://host:8080/...)". Both call
  sites now render the named members.error.inviteFailed message; a new
  inviteFailureReason() helper in vaultShare.ts collapses any URL-bearing
  message to a translated "sync server could not be reached" reason, reused
  by the toast formatter too so it doesn't leak there either. The dropdown
  now closes when an error is set, since it was rendering on top of the
  error text.

- Unticking every role chip left `selectedRoleIds` empty, and the code
  silently fell back to "member" — not the least-privileged role. The
  invite and email-invite actions are now disabled with an inline hint
  until a role is picked; a new leastPrivilegedRole() in vaultShareModel.ts
  is kept as a defensive fallback in the invite path, never relied on by
  the UI.
@kipavy
kipavy merged commit 653a8b3 into dev Aug 25, 2026
4 checks passed
@kipavy
kipavy deleted the feat/vault-share-sheet branch August 25, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant