Skip to content

feat: general invite link per stack + remove member - #161

Open
brendaasfjnso wants to merge 14 commits into
developmentfrom
feat/general-invite
Open

feat: general invite link per stack + remove member#161
brendaasfjnso wants to merge 14 commits into
developmentfrom
feat/general-invite

Conversation

@brendaasfjnso

Copy link
Copy Markdown

Summary

  • General invite link: Stack creation modal now shows ONE shareable invite link instead of N-1 individual links. Anyone with the link can join — the backend serves available nonces from a new /api/stacks/invite/claim endpoint.
  • Remove member button: Stack page now shows a "Remove" button beside each member (owner only, pre-launch stacks). Triggers a confirmation modal warning that removal decommissions the stack.
  • Updated UX copy: Highlights that the link can invite any member and that users can manage/remove members before launching.

Changed files

  • src/components/modal/modals/stack-result.tsx — Single invite link generation + updated copy
  • src/app/api/stacks/invite/claim/route.ts — New API to claim available nonce for general links
  • src/app/stacks/join/_components/accept-invite.tsx — Handles general links without nonce/signature
  • src/app/stacks/join/page.tsx — Detects general vs direct invite links
  • src/app/stacks/[id]/_components/members-info.tsx — Remove button per member
  • src/app/stacks/[id]/_components/members.tsx — General invite link display + isOwner prop
  • src/components/modal/modals/remove-member-warning.tsx — Confirmation modal for removal
  • src/components/modal/context.tsx + presenter.tsx — REMOVE_MEMBER_WARNING modal type

Test plan

  • Create a new stack and verify ONE invite link appears in the success modal
  • Copy the general invite link and open it in another browser/wallet — verify it claims a nonce and allows joining
  • Visit the stack page as owner and verify the invite link is shown in the members section
  • Verify "Remove" button appears beside non-owner members when stack is pending-start
  • Click Remove and verify the confirmation modal appears with decommission warning
  • Verify old direct invite links (with nonce/signature) still work for backwards compatibility

🤖 Generated with Claude Code

…nk per stack

- Stack creation modal now shows ONE shareable invite link instead of N-1 individual links
- New /api/stacks/invite/claim endpoint serves available nonces to general link visitors
- Join page handles general links (no nonce/signature in URL) by claiming from API
- Added "Remove" button beside each member on the stack page (owner only, pre-launch)
- Remove triggers decommission with a confirmation warning modal
- Updated UX copy to highlight link sharing and member management before launch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploy Preview for app-stacks failed. Why did it fail? →

Name Link
🔨 Latest commit bb28b6c
🔍 Latest deploy log https://app.netlify.com/projects/app-stacks/deploys/6a8ac398ac20fb0008467adb

The stack page built the link with empty duration= and deposit= params, so
invite-details rendered a blank term and computed $0.00 for both the deposit
amount and the stack goal (+"" coerces to 0). Both values are already on the
circle, so format them the way the creation modal does.
The warning is still accurate for the legacy per-member links, which are the
only ones that dead-end when their nonce is spent. General links claim a nonce
from the pool, so it stays hidden for those.

@franrolotti franrolotti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functionally this works — I traced the general-link path end to end and the claim → redeemInvitePATCH /api/stacks/invite cycle is sound. Happy to approve once the Remove button is dealt with.

I pushed three commits to this branch: a merge with development plus two small fixes (details at the bottom).

The Remove button can't work yet — suggest dropping it from this PR.

remove-member-warning.tsx:25 calls decommission(circleId), but decommission is onlyActive, and isActive is only set inside start(). The contract reports NotStarted exactly when !isActive, which is what "pending-start" resolves to in get-user-circle-status.ts:122 — the same condition the button is gated on (members-info.tsx:135). So it renders if and only if the transaction reverts with NotActive. Past that guard it would still fail _isDecommissionable, which needs currentRound > 0 and a missed deposit window; decommission unwinds a stalled live circle and refunds deposits, it was never a pre-launch cancel.

There's no function on the deployed implementation that removes a member. saving-circles#feat/add-members adds removeMember(id, member) with exactly the right guards (pre-start only, owner or the member themselves, owner not removable), but it needs a proxy upgrade. I'd cut the button, modal, and REMOVE_MEMBER_WARNING type here and bring them back in a follow-up once that lands — it's all subtractive, and the general-link half is good to ship on its own.

Claim/redeem race. claim/route.ts:31 returns the first used: false link but doesn't reserve it; used is only written by the PATCH after a successful redeem. Two people claiming at once get the same nonce and the second redeemInvite reverts InviteAlreadyUsed. The retry path recovers, so it's a rough edge rather than a break — but someone who claims and walks away leaves that nonce served to everyone until another visitor actually redeems it. Marking it used at claim time trades this for a worse bug (abandoned claims burn slots permanently), so the real fix is probably a reserved_at column with an expiry. Fine to defer, worth an issue.

Invites are still bearer tokens. /api/stacks/invite/claim returns a valid nonce + owner signature to anyone who passes a circleId, with no auth — and circle ids are sequential. Nothing is broken by this, but combined with a publicly shareable link it means anyone who guesses an id can take a slot without ever receiving the invite. The signature covers Invite(id, nonce) only, so it can't be bound to a wallet without the same contract upgrade as above. Worth tracking together with the removeMember work rather than fixing here.


What I pushed

  • 7c6d0f1 — merge with development, which the branch needed. One adjacency conflict in the imports of members-info.tsx (ff63cef deleted import Link, this PR added two imports next to it); the alias/ENS refactor auto-merged cleanly with your memberDisplayName change.
  • f7087b0members.tsx:101 built the link with empty duration=&deposit=, so the invitee's preview showed a blank term and $0.00 for both the deposit and the stack goal (+"" is 0). The creation modal passes real values, so the same stack previewed differently depending on which link was shared. Both values are already on circle.
  • 3e81aaa — the "this invite can only be accepted once" alert was dropped for all links; it's still accurate for the legacy per-member ones, so it's back behind !isGeneralLink.

Revert any of them if you disagree.

The eslint config treats prettier/prettier as an error, so the unwrapped
call failed `next build` on the deploy preview.
The prior general-link work still pre-signed a pool of N-1 EIP-712 invites
per stack (up to 24 wallet prompts) and served them one-at-a-time through a
claim endpoint. Replace it with a single circleId-only invite link: visitors
request to join with no signature and no on-chain action, and the owner
reviews requests and accepts them via the contract's new addMembers
(bulk-capable, no signature) or dismisses them. removeMember replaces the old
decommission-the-whole-stack workaround for removing a single pre-launch
member.

- Bump the saving-circles submodule to pick up addMembers/removeMember and
  add both to the frontend ABI.
- New join_requests table (service-role only) and /api/stacks/join-request
  (request/list/decide) and /api/stacks/member (cleanup on removal) routes;
  drop the old pool-claim endpoint and stacks_metadata.invite_links.
- Stack creation, the join page, and the owner's member list are reworked
  around the new flow, including bulk-accept for multiple pending requests
  in a single addMembers transaction.
- Fix: invalidate wagmi's read caches after removeMember succeeds so the
  member list actually updates instead of only refreshing the
  server-rendered page.
…al split

The Makefile now persists Anvil state across restarts and separates
"just start the dev server" from "wipe off-chain data, redeploy, and
start" — bring the README's setup and workflow instructions in line.
…ionSettings

Picks up the self-authorized member and automation-settings migration
functions the wallet-migration flow calls.
Temporary: points at file:../bread-ui-kit so the wallet-migration flow
can pick up the connected-user EOA fallback fix before it's published.
Revert to a published @breadcoop/ui version once bread-ui-kit's
feat/connected-user-eoa-fallback branch lands and ships a release.
…ount overrides

Adds the two new contract functions to the frontend ABIs, and lets
useSavingCirclesTx/useAutomaticSavingCirclesTx send a tx from an
explicit account (e.g. an embedded wallet acting as msg.sender) instead
of always defaulting to the currently connected wallet.
Adds hooks to detect a genuinely linked external wallet (as opposed to
the embedded wallet wagmi auto-connects) and to prefer it as the
circle member address once it's actually on-chain, so claims and stack
pages keep working through a migration. claim-button now calls
withdrawFor with the effective member instead of a plain withdraw.
Adds transferred_to_wallet_at, migrated_stacks_at, and a
last_migration_result jsonb column (externalAddress/failed/attemptedAt)
on users, plus hooks and a PATCH /api/user path to read and write them.
last_migration_result records which parts of a migration attempt
failed (e.g. "circle:3", "circle:5:automation", "transfer:funds") so a
partial failure can be retried on a later visit instead of silently
looking done.
…allet users

Surfaces a single banner + modal for users who linked an external
wallet but still have stacks and/or funds sitting in their old
Privy-created embedded wallet. On confirm it migrates each circle's
membership (migrateMember) and automation opt-ins
(migrateAutomationSettings), then sweeps any BREAD/xDAI — all sponsored
from the embedded wallet with no wallet-UI prompts. Supabase's
wallet_address only flips to the linked wallet once something has
actually moved on-chain, so alias/dashboard lookups keep resolving to
the old address until then. Also reopens for any circle whose core
membership moved but whose automation settings didn't, using
last_migration_result to find them.
Was temporarily set to "all-users" to create legacy embedded-wallet
test accounts for exercising the migration flow.
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.

3 participants