From 5332f77831a4113332c104de0fd75eb756d709bb Mon Sep 17 00:00:00 2001 From: Arnas Donauskas Date: Thu, 4 Jun 2026 17:39:46 +0300 Subject: [PATCH] =?UTF-8?q?feat(members):=20GA=20the=20Members=20tab=20?= =?UTF-8?q?=E2=80=94=20remove=20account=5Fsharing=20beta=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multi-user account-sharing surface (Members tab + invite/role management APIs + /join redeem flow) is complete, so drop the `account_sharing` beta flag that hid it during development. Mirrors the Flows soft-GA in 0.2.0. - settings: Members tab is now always rendered (no beta_features check, no stale-link fallback). The `profiles.beta_features` column itself stays for future betas. - sidebar: the active-account strip no longer keys off the flag. It now shows whenever the account name differs from the user's own name — i.e. once you've renamed the account or joined a shared one — preserving the original "don't duplicate a solo user's name" behaviour without the flag. - CHANGELOG/README: document Members as a shipped, all-users feature. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 44 ++++++++++++++------- README.md | 4 ++ src/app/(dashboard)/settings/page.tsx | 56 +++++++-------------------- src/components/layout/sidebar.tsx | 37 +++++++++--------- 4 files changed, 66 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 527dcec..233022d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,14 +11,17 @@ and polish. ## [Unreleased] -Foundation for multi-user accounts. Every wacrm install becomes -multi-tenant on the database side: a single user's signup creates a -fresh "account", and every row is scoped to that account rather than -to the user directly. The user-visible invite / members surface lands -in follow-up PRs gated by the `'account_sharing'` beta feature flag — -this release is wiring with no behaviour change on its own. Existing +Multi-user accounts ship. Every wacrm install is multi-tenant on the +database side: a single user's signup creates a fresh "account", and +every row is scoped to that account rather than to the user directly. +This release also opens the user-visible **Members** surface — invite +teammates by link, manage their roles, transfer ownership — to all +users. The `'account_sharing'` beta gate that hid it during +development is removed (mirrors the Flows soft-GA in 0.2.0). Existing self-hosted instances keep working: every existing user is backfilled -as the sole owner of their own account and sees identical data. +as the sole owner of their own account and sees identical data, and a +solo owner who never invites anyone sees the same single-user app they +always did. ### Changed @@ -71,9 +74,15 @@ as the sole owner of their own account and sees identical data. silently broken to a teammate looking at a feature they don't yet have permission for. - **Sidebar surfaces the active account** above the user info - when the `account_sharing` beta flag is on. Solo users keep - the original layout (their account is named after them, so - duplicating it would just add visual noise). + whenever the account name differs from your own — i.e. once + you've renamed the account or joined a shared one. A default + solo account is named after you, so the strip stays hidden to + avoid duplicating your name in the footer. +- **Members is open to all users.** The `account_sharing` beta + flag that hid the Settings → Members tab and the sidebar + account strip during development is gone; the multi-user + surface is now part of the standard app. (Same soft-GA move as + Flows in 0.2.0.) ### Fixed @@ -91,8 +100,17 @@ as the sole owner of their own account and sees identical data. ### Added +- **Members tab in Settings.** The user-facing surface for the + multi-user APIs below, available to everyone (no beta flag). From + Settings → **Members** an admin or owner can: see who's on the + account with their role and join date, invite teammates by + generating a one-time share link (pick the role + optional + expiry), revoke pending invites, change a member's role, remove a + member, and — as owner — transfer ownership. Recipients accept via + a public `/join/[token]` page. Full guide: + [Members docs](https://wacrm.tech/docs/members). - **Account & member management API** — server-side endpoints - for the upcoming Members tab UI. All routes are role-gated and + backing the Members tab. All routes are role-gated and return Supabase-RLS-scoped data. - `GET /api/account` — caller's account + role. Any member. - `PATCH /api/account` — rename the account. Admin+. @@ -108,8 +126,8 @@ as the sole owner of their own account and sees identical data. - `POST /api/account/transfer-ownership` — owner only. Atomic swap with the named member. - **Invitation API + redeem flow** — the no-email, link-only - invite path. Backend is complete; the Members tab UI that - drives it lands in a follow-up. + invite path that powers the Members tab's "Invite member" button + and the `/join/[token]` accept page. - `GET /api/account/invitations` — list outstanding (admin+). - `POST /api/account/invitations` — create an invite, returns the plaintext token + share URL **exactly once** (we store diff --git a/README.md b/README.md index 69bb8f4..f9dbb0a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ clone or fork it to run your own CRM. tags, webhooks. Visual builder. - **Real-time dashboard** — response times, daily volume, pipeline value, cross-module activity feed. +- **Team accounts** — invite teammates by link, role-based access + (owner / admin / agent / viewer), ownership transfer. Every install + is account-scoped, so one shared inbox can be staffed by a whole + team. Solo use stays single-user with zero setup. - **Account management** — email, password, avatar, global sign-out. ## Why fork this? diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx index 91e7298..f8e3c3f 100644 --- a/src/app/(dashboard)/settings/page.tsx +++ b/src/app/(dashboard)/settings/page.tsx @@ -18,54 +18,31 @@ import { PasswordForm } from '@/components/settings/password-form'; import { SessionsCard } from '@/components/settings/sessions-card'; import { AppearancePanel } from '@/components/settings/appearance-panel'; import { MembersTab } from '@/components/settings/members-tab'; -import { useAuth } from '@/hooks/use-auth'; -const BASE_TAB_VALUES = [ +const TAB_VALUES = [ 'profile', 'whatsapp', 'templates', 'tags', 'appearance', + 'members', ] as const; -const FLAGGED_TAB_VALUES = ['members'] as const; -const TAB_VALUES = [...BASE_TAB_VALUES, ...FLAGGED_TAB_VALUES] as const; type TabValue = (typeof TAB_VALUES)[number]; function isTabValue(v: string | null): v is TabValue { return !!v && (TAB_VALUES as readonly string[]).includes(v); } -// Flag key matches what migration 011 introduced. The Members tab -// stays hidden until the user's profile.beta_features array contains -// this string; flip it via Supabase Studio: -// UPDATE profiles SET beta_features = beta_features || ARRAY['account_sharing'] -// WHERE user_id = ''; -const ACCOUNT_SHARING_FLAG = 'account_sharing'; - export default function SettingsPage() { const router = useRouter(); const searchParams = useSearchParams(); - const { profile, profileLoading } = useAuth(); - - const accountSharingEnabled = - !profileLoading && - !!profile?.beta_features?.includes(ACCOUNT_SHARING_FLAG); // The URL is the single source of truth for the active tab — no // local state, no sync effect. A previous revision duplicated this // into `useState` + a sync effect, which tripped React 19's // set-state-in-effect rule and was also redundant. const queryTab = searchParams.get('tab'); - const requestedTab: TabValue = isTabValue(queryTab) ? queryTab : 'profile'; - - // If a user lands on /settings?tab=members but the flag is off - // (e.g. their profile was reset, or they followed a stale link), - // fall back to the profile tab silently rather than rendering an - // empty TabsContent. - const tab: TabValue = - requestedTab === 'members' && !accountSharingEnabled - ? 'profile' - : requestedTab; + const tab: TabValue = isTabValue(queryTab) ? queryTab : 'profile'; const onChange = (next: TabValue) => { const params = new URLSearchParams(searchParams.toString()); @@ -120,18 +97,13 @@ export default function SettingsPage() { Appearance - {/* Members tab is feature-flagged. We render the trigger - only when the flag is enabled, so users without the - flag see the original 5-tab layout. */} - {accountSharingEnabled && ( - - - Members - - )} + + + Members + @@ -156,11 +128,9 @@ export default function SettingsPage() { - {accountSharingEnabled && ( - - - - )} + + + ); diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index c8f1ab0..28d2c89 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -106,23 +106,22 @@ interface SidebarProps { onClose?: () => void; } -// Same flag key the Members tab and Settings page use. Flip per -// profile via Supabase Studio to dogfood the multi-user surface. -const ACCOUNT_SHARING_FLAG = "account_sharing"; - export function Sidebar({ open = false, onClose }: SidebarProps) { const pathname = usePathname(); const { profile, profileLoading, account, accountRole, signOut } = useAuth(); const totalUnread = useTotalUnread(); - // Match the settings page's check: only treat the flag as enabled - // once the profile has finished loading. Without this, the strip - // would briefly flash absent during the initial profile fetch - // (when `profile` is null and the boolean coerces to false), then - // pop in once the row resolves — visible as a layout jump in the - // sidebar footer. - const accountSharingEnabled = + // Only surface the account-name strip when it actually carries + // information. A solo user's personal account is named after them + // (the 017 signup trigger seeds it from `full_name`), so showing it + // here would just duplicate the user name in the footer below. Once + // the account is renamed or the user joins a shared account, the + // name diverges and the strip becomes meaningful — that's the signal + // we gate on. Wait for the profile fetch to settle first, otherwise + // the strip flashes in once the row resolves (a layout jump). + const showAccountStrip = !profileLoading && - !!profile?.beta_features?.includes(ACCOUNT_SHARING_FLAG); + !!account?.name && + account.name !== profile?.full_name; // Close the drawer when route changes — users opened it to navigate, // so once they pick a destination the drawer should get out of the way. @@ -272,13 +271,13 @@ export function Sidebar({ open = false, onClose }: SidebarProps) { {/* User section */}
- {/* Account name display — only surfaced when the user is - opted into the account_sharing beta flag. For solo - users (the default) the account is named after them, - so showing it here would just duplicate the user name - below. Once the flag is on the user is at minimum - aware of which shared account they're acting in. */} - {accountSharingEnabled && account?.name ? ( + {/* Account name display — surfaced only when the account + name differs from the user's own name (see + `showAccountStrip`). For a default solo account the two + match, so we hide it to avoid duplicating the user name + below; for renamed or shared accounts it tells the user + which account they're acting in. */} + {showAccountStrip && account?.name ? (
{/* `title=` exposes the full name on hover when it