mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
Merge pull request #204 from ArnasDon/ga/members-account-sharing
feat(members): GA the Members tab (remove account_sharing beta gate)
This commit is contained in:
commit
d6dbbf22b2
44
CHANGELOG.md
44
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
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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 = '<theirs>';
|
||||
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() {
|
||||
<Palette className="size-4" />
|
||||
Appearance
|
||||
</TabsTrigger>
|
||||
{/* 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 && (
|
||||
<TabsTrigger
|
||||
value="members"
|
||||
className="data-active:bg-slate-800 data-active:text-primary text-slate-400"
|
||||
>
|
||||
<UsersRound className="size-4" />
|
||||
Members
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger
|
||||
value="members"
|
||||
className="data-active:bg-slate-800 data-active:text-primary text-slate-400"
|
||||
>
|
||||
<UsersRound className="size-4" />
|
||||
Members
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile" className="space-y-6">
|
||||
@ -156,11 +128,9 @@ export default function SettingsPage() {
|
||||
<AppearancePanel />
|
||||
</TabsContent>
|
||||
|
||||
{accountSharingEnabled && (
|
||||
<TabsContent value="members">
|
||||
<MembersTab />
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="members">
|
||||
<MembersTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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 */}
|
||||
<div className="shrink-0 border-t border-slate-800 p-3">
|
||||
{/* 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 ? (
|
||||
<div className="mb-2 flex items-center gap-2 px-3 text-xs text-slate-500">
|
||||
<UsersRound className="size-3.5 shrink-0" />
|
||||
{/* `title=` exposes the full name on hover when it
|
||||
|
||||
Loading…
Reference in New Issue
Block a user