authfyio-react 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/client.d.ts +128 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +227 -0
- package/dist/components.d.ts +79 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +111 -0
- package/dist/hooks-flow.d.ts +59 -0
- package/dist/hooks-flow.d.ts.map +1 -0
- package/dist/hooks-flow.js +133 -0
- package/dist/hooks.d.ts +113 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +212 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/jwt.d.ts +20 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +30 -0
- package/dist/provider.d.ts +50 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +91 -0
- package/dist/ui/CheckoutButton.d.ts +14 -0
- package/dist/ui/CheckoutButton.d.ts.map +1 -0
- package/dist/ui/CheckoutButton.js +28 -0
- package/dist/ui/Control.d.ts +17 -0
- package/dist/ui/Control.d.ts.map +1 -0
- package/dist/ui/Control.js +33 -0
- package/dist/ui/CreateOrganization.d.ts +19 -0
- package/dist/ui/CreateOrganization.d.ts.map +1 -0
- package/dist/ui/CreateOrganization.js +75 -0
- package/dist/ui/OrganizationList.d.ts +13 -0
- package/dist/ui/OrganizationList.d.ts.map +1 -0
- package/dist/ui/OrganizationList.js +47 -0
- package/dist/ui/OrganizationProfile.d.ts +12 -0
- package/dist/ui/OrganizationProfile.d.ts.map +1 -0
- package/dist/ui/OrganizationProfile.js +116 -0
- package/dist/ui/OrganizationSwitcher.d.ts +17 -0
- package/dist/ui/OrganizationSwitcher.d.ts.map +1 -0
- package/dist/ui/OrganizationSwitcher.js +59 -0
- package/dist/ui/PricingTable.d.ts +15 -0
- package/dist/ui/PricingTable.d.ts.map +1 -0
- package/dist/ui/PricingTable.js +74 -0
- package/dist/ui/SignIn.d.ts +23 -0
- package/dist/ui/SignIn.d.ts.map +1 -0
- package/dist/ui/SignIn.js +489 -0
- package/dist/ui/SignUp.d.ts +18 -0
- package/dist/ui/SignUp.d.ts.map +1 -0
- package/dist/ui/SignUp.js +153 -0
- package/dist/ui/UnstyledButtons.d.ts +24 -0
- package/dist/ui/UnstyledButtons.d.ts.map +1 -0
- package/dist/ui/UnstyledButtons.js +42 -0
- package/dist/ui/UserAvatar.d.ts +14 -0
- package/dist/ui/UserAvatar.d.ts.map +1 -0
- package/dist/ui/UserAvatar.js +52 -0
- package/dist/ui/UserButton.d.ts +15 -0
- package/dist/ui/UserButton.d.ts.map +1 -0
- package/dist/ui/UserButton.js +82 -0
- package/dist/ui/UserProfile.d.ts +19 -0
- package/dist/ui/UserProfile.d.ts.map +1 -0
- package/dist/ui/UserProfile.js +199 -0
- package/dist/ui/index.d.ts +14 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +13 -0
- package/dist/ui/styles.d.ts +10 -0
- package/dist/ui/styles.d.ts.map +1 -0
- package/dist/ui/styles.js +291 -0
- package/package.json +46 -0
package/dist/provider.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { AuthfyioReactClient } from './client.js';
|
|
4
|
+
/**
|
|
5
|
+
* Default browser-side base URL. Resolves to the same-origin proxy mounted
|
|
6
|
+
* at `/api/af` (provided by `authfyio-nextjs/proxy`) so cookies set by
|
|
7
|
+
* the upstream API land on the customer's origin.
|
|
8
|
+
*
|
|
9
|
+
* For non-Next setups you can pass `baseUrl="https://api.authfyio.com"`
|
|
10
|
+
* directly, but you must arrange same-origin cookie scoping yourself.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_PROVIDER_BASE_URL = '/api/af';
|
|
13
|
+
const Ctx = createContext(null);
|
|
14
|
+
export function AuthfyioProvider(props) {
|
|
15
|
+
const baseUrl = props.baseUrl ?? DEFAULT_PROVIDER_BASE_URL;
|
|
16
|
+
// Accept the publishable key via prop *or* env var (Next inlines
|
|
17
|
+
// NEXT_PUBLIC_* into the bundle at build time).
|
|
18
|
+
const publishableKey = props.publishableKey ??
|
|
19
|
+
(typeof process !== 'undefined' ? process.env?.NEXT_PUBLIC_AUTHFYIO_PUBLISHABLE_KEY : undefined);
|
|
20
|
+
const client = useMemo(() => new AuthfyioReactClient({ baseUrl, publishableKey }), [baseUrl, publishableKey]);
|
|
21
|
+
// Always start signed-out so the SSR markup matches the client's first
|
|
22
|
+
// render. `document.cookie` doesn't exist on the server, so reading it in
|
|
23
|
+
// useState's initializer would diverge between server and client and
|
|
24
|
+
// trip React's hydration guard. The cookie probe runs in the effect
|
|
25
|
+
// below, after mount.
|
|
26
|
+
const [session, setSession] = useState({ status: 'signed_out' });
|
|
27
|
+
const refreshing = useRef(false);
|
|
28
|
+
const refresh = async () => {
|
|
29
|
+
if (refreshing.current)
|
|
30
|
+
return;
|
|
31
|
+
refreshing.current = true;
|
|
32
|
+
try {
|
|
33
|
+
const next = await client.refresh();
|
|
34
|
+
if (!next.ok) {
|
|
35
|
+
setSession({ status: 'signed_out' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setSession({ status: 'signed_in', jwt: next.jwt, claims: next.claims });
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
refreshing.current = false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const now = client.getSessionFromCookies();
|
|
46
|
+
if (now) {
|
|
47
|
+
setSession({ status: 'signed_in', jwt: now.jwt, claims: now.claims });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// No `__session` cookie in the browser — but the long-lived `__client`
|
|
51
|
+
// cookie may still be valid (it's httpOnly so we can't read it from JS).
|
|
52
|
+
// Try a refresh once: if it succeeds we recover the session without
|
|
53
|
+
// forcing the user to re-sign-in. We swallow failures because for
|
|
54
|
+
// genuinely-signed-out visitors the refresh endpoint returns 401.
|
|
55
|
+
setSession({ status: 'signed_out' });
|
|
56
|
+
refresh().catch(() => { });
|
|
57
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
|
+
}, [client]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (props.autoRefresh === false)
|
|
61
|
+
return;
|
|
62
|
+
if (session.status !== 'signed_in')
|
|
63
|
+
return;
|
|
64
|
+
const exp = session.claims.exp;
|
|
65
|
+
if (!exp)
|
|
66
|
+
return;
|
|
67
|
+
const skew = props.refreshSkewSeconds ?? 15;
|
|
68
|
+
const msUntil = Math.max(0, exp * 1000 - Date.now() - skew * 1000);
|
|
69
|
+
const t = window.setTimeout(() => {
|
|
70
|
+
refresh().catch(() => { });
|
|
71
|
+
}, msUntil);
|
|
72
|
+
return () => window.clearTimeout(t);
|
|
73
|
+
}, [session, props.autoRefresh, props.refreshSkewSeconds]);
|
|
74
|
+
const value = useMemo(() => ({ client, session, refresh }), [client, session]);
|
|
75
|
+
return _jsx(Ctx.Provider, { value: value, children: props.children });
|
|
76
|
+
}
|
|
77
|
+
export function useAuthfyio() {
|
|
78
|
+
const ctx = useContext(Ctx);
|
|
79
|
+
if (!ctx)
|
|
80
|
+
throw new Error('AuthfyioProvider missing');
|
|
81
|
+
return ctx;
|
|
82
|
+
}
|
|
83
|
+
export function useSession() {
|
|
84
|
+
return useAuthfyio().session;
|
|
85
|
+
}
|
|
86
|
+
export function useUserId() {
|
|
87
|
+
const s = useSession();
|
|
88
|
+
if (s.status !== 'signed_in')
|
|
89
|
+
return null;
|
|
90
|
+
return s.claims.sub ?? null;
|
|
91
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type CheckoutButtonProps = {
|
|
3
|
+
planId: string;
|
|
4
|
+
billingCycle?: 'monthly' | 'annual';
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* One-line button that opens a Stripe Checkout session for a specific plan.
|
|
10
|
+
* Inherits your app's button styling
|
|
11
|
+
* if you pass `className`; falls back to a minimal primary button otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export declare function CheckoutButton({ planId, billingCycle, children, className }: CheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=CheckoutButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CheckoutButton.d.ts","sourceRoot":"","sources":["../../src/ui/CheckoutButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACpC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,YAAwB,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,mBAAmB,2CA+B5G"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCheckout } from '../hooks.js';
|
|
4
|
+
/**
|
|
5
|
+
* One-line button that opens a Stripe Checkout session for a specific plan.
|
|
6
|
+
* Inherits your app's button styling
|
|
7
|
+
* if you pass `className`; falls back to a minimal primary button otherwise.
|
|
8
|
+
*/
|
|
9
|
+
export function CheckoutButton({ planId, billingCycle = 'monthly', children, className }) {
|
|
10
|
+
const { startCheckout, isLoading } = useCheckout();
|
|
11
|
+
return (_jsx("button", { type: "button", className: className, disabled: isLoading, onClick: () => startCheckout(planId, billingCycle), style: className
|
|
12
|
+
? undefined
|
|
13
|
+
: {
|
|
14
|
+
display: 'inline-flex',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
height: 40,
|
|
18
|
+
padding: '0 16px',
|
|
19
|
+
fontSize: 13.5,
|
|
20
|
+
fontWeight: 500,
|
|
21
|
+
color: '#fff',
|
|
22
|
+
background: 'var(--af-primary, #4f3df0)',
|
|
23
|
+
border: 0,
|
|
24
|
+
borderRadius: 10,
|
|
25
|
+
cursor: isLoading ? 'not-allowed' : 'pointer',
|
|
26
|
+
fontFamily: 'inherit',
|
|
27
|
+
}, children: isLoading ? 'Opening checkout…' : children ?? 'Upgrade' }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Renders children only after the Authfyio provider has hydrated — i.e.
|
|
4
|
+
* once the cookie has been read and the session state resolved on the
|
|
5
|
+
* client.
|
|
6
|
+
*/
|
|
7
|
+
export declare function AuthfyioLoaded({ children }: {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}): React.ReactElement | null;
|
|
10
|
+
/**
|
|
11
|
+
* Renders children ONLY while the provider is still hydrating. Useful for
|
|
12
|
+
* spinners and skeleton placeholders.
|
|
13
|
+
*/
|
|
14
|
+
export declare function AuthfyioLoading({ children }: {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}): React.ReactElement | null;
|
|
17
|
+
//# sourceMappingURL=Control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Control.d.ts","sourceRoot":"","sources":["../../src/ui/Control.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAMrG;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAKtG"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useAuthfyio } from '../provider.js';
|
|
5
|
+
/**
|
|
6
|
+
* Renders children only after the Authfyio provider has hydrated — i.e.
|
|
7
|
+
* once the cookie has been read and the session state resolved on the
|
|
8
|
+
* client.
|
|
9
|
+
*/
|
|
10
|
+
export function AuthfyioLoaded({ children }) {
|
|
11
|
+
const loaded = useClientMounted();
|
|
12
|
+
// Throws if provider is missing — which is what we want so callers notice.
|
|
13
|
+
useAuthfyio();
|
|
14
|
+
if (!loaded)
|
|
15
|
+
return null;
|
|
16
|
+
return _jsx(_Fragment, { children: children });
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Renders children ONLY while the provider is still hydrating. Useful for
|
|
20
|
+
* spinners and skeleton placeholders.
|
|
21
|
+
*/
|
|
22
|
+
export function AuthfyioLoading({ children }) {
|
|
23
|
+
const loaded = useClientMounted();
|
|
24
|
+
useAuthfyio();
|
|
25
|
+
if (loaded)
|
|
26
|
+
return null;
|
|
27
|
+
return _jsx(_Fragment, { children: children });
|
|
28
|
+
}
|
|
29
|
+
function useClientMounted() {
|
|
30
|
+
const [mounted, setMounted] = useState(false);
|
|
31
|
+
useEffect(() => setMounted(true), []);
|
|
32
|
+
return mounted;
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type CreateOrganizationProps = {
|
|
2
|
+
/** Redirect target after the org is created. Default: reload. */
|
|
3
|
+
afterCreateOrganizationUrl?: string;
|
|
4
|
+
/** Fires with the new org id BEFORE the redirect. */
|
|
5
|
+
onCreated?: (orgId: string) => void;
|
|
6
|
+
title?: string;
|
|
7
|
+
subtitle?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Self-service organization creation form. Auto-generates a URL-friendly
|
|
11
|
+
* slug from the name field unless the user overrides it. The role-set
|
|
12
|
+
* applied to the new org comes from the tenant's `defaultRoleSet`
|
|
13
|
+
* configured under Configure → Organization settings — there's no UI
|
|
14
|
+
* choice here because picking it per-org would be more confusion than
|
|
15
|
+
* value (the dashboard owner already decided which set is canonical).
|
|
16
|
+
* Creator's own role is always 'owner'.
|
|
17
|
+
*/
|
|
18
|
+
export declare function CreateOrganization({ afterCreateOrganizationUrl, onCreated, title, subtitle, }: CreateOrganizationProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=CreateOrganization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CreateOrganization.d.ts","sourceRoot":"","sources":["../../src/ui/CreateOrganization.tsx"],"names":[],"mappings":"AAOA,MAAM,MAAM,uBAAuB,GAAG;IACpC,iEAAiE;IACjE,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,qDAAqD;IACrD,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,0BAA0B,EAC1B,SAAS,EACT,KAAmC,EACnC,QAAkF,GACnF,EAAE,uBAAuB,2CA4FzB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useAuthfyio } from '../provider.js';
|
|
5
|
+
import { ensureStylesInjected } from './styles.js';
|
|
6
|
+
/**
|
|
7
|
+
* Self-service organization creation form. Auto-generates a URL-friendly
|
|
8
|
+
* slug from the name field unless the user overrides it. The role-set
|
|
9
|
+
* applied to the new org comes from the tenant's `defaultRoleSet`
|
|
10
|
+
* configured under Configure → Organization settings — there's no UI
|
|
11
|
+
* choice here because picking it per-org would be more confusion than
|
|
12
|
+
* value (the dashboard owner already decided which set is canonical).
|
|
13
|
+
* Creator's own role is always 'owner'.
|
|
14
|
+
*/
|
|
15
|
+
export function CreateOrganization({ afterCreateOrganizationUrl, onCreated, title = 'Create a new organization', subtitle = "Organizations are how you collaborate with teammates inside your app.", }) {
|
|
16
|
+
useEffect(ensureStylesInjected, []);
|
|
17
|
+
const { client } = useAuthfyio();
|
|
18
|
+
const baseUrl = client.baseUrl;
|
|
19
|
+
const [name, setName] = useState('');
|
|
20
|
+
const [slug, setSlug] = useState('');
|
|
21
|
+
const [slugTouched, setSlugTouched] = useState(false);
|
|
22
|
+
const [busy, setBusy] = useState(false);
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
// Auto-derive slug from name until the user edits it.
|
|
25
|
+
const suggestedSlug = slugify(name);
|
|
26
|
+
const effectiveSlug = slugTouched ? slug : suggestedSlug;
|
|
27
|
+
async function submit(e) {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
setError(null);
|
|
30
|
+
setBusy(true);
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`${baseUrl}/v1/orgs`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
credentials: 'include',
|
|
35
|
+
headers: { 'content-type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ name, slug: effectiveSlug }),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const body = await res.json().catch(() => ({}));
|
|
40
|
+
throw new Error(body?.message ?? 'Could not create organization.');
|
|
41
|
+
}
|
|
42
|
+
const body = (await res.json());
|
|
43
|
+
if (onCreated)
|
|
44
|
+
onCreated(body.orgId);
|
|
45
|
+
// Switch the session to the newly created org.
|
|
46
|
+
await fetch(`${baseUrl}/v1/orgs/current`, {
|
|
47
|
+
method: 'PATCH',
|
|
48
|
+
credentials: 'include',
|
|
49
|
+
headers: { 'content-type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({ orgId: body.orgId }),
|
|
51
|
+
});
|
|
52
|
+
if (typeof window !== 'undefined') {
|
|
53
|
+
window.location.href = afterCreateOrganizationUrl ?? window.location.pathname;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
setError(err?.message ?? 'Could not create organization.');
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
setBusy(false);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return (_jsxs("div", { className: "af-card", role: "form", "aria-label": "Create organization", children: [_jsx("h1", { className: "af-title", children: title }), _jsx("p", { className: "af-subtitle", children: subtitle }), _jsxs("form", { onSubmit: submit, children: [_jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", children: "Organization name" }), _jsx("input", { className: "af-input", type: "text", required: true, value: name, onChange: (e) => setName(e.target.value), placeholder: "Acme Inc.", maxLength: 120 })] }), _jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", children: "URL slug" }), _jsx("input", { className: "af-input", type: "text", required: true, pattern: "[a-z0-9-]+", value: effectiveSlug, onChange: (e) => {
|
|
64
|
+
setSlug(e.target.value);
|
|
65
|
+
setSlugTouched(true);
|
|
66
|
+
}, placeholder: "acme", maxLength: 60 }), _jsx("p", { style: { marginTop: 6, fontSize: 11.5, color: '#6b7280' }, children: "Lowercase letters, numbers, and hyphens." })] }), _jsx("div", { className: "af-primary-row", children: _jsx("button", { type: "submit", className: "af-btn af-btn-primary", disabled: busy || !name.trim(), children: busy ? 'Creating…' : 'Create organization' }) })] }), error && _jsx("div", { className: "af-error", children: error })] }));
|
|
67
|
+
}
|
|
68
|
+
function slugify(input) {
|
|
69
|
+
return input
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.trim()
|
|
72
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
73
|
+
.replace(/^-+|-+$/g, '')
|
|
74
|
+
.slice(0, 60);
|
|
75
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type OrganizationListProps = {
|
|
2
|
+
/** Where to land when the user clicks an org. Must include `{slug}` or `{id}`.
|
|
3
|
+
* Default '/org/{slug}'. */
|
|
4
|
+
hrefPattern?: string;
|
|
5
|
+
/** Path to the create-organization flow. */
|
|
6
|
+
createOrganizationUrl?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* List view of every organization the signed-in user belongs to. Useful
|
|
10
|
+
* as a full-page picker after login.
|
|
11
|
+
*/
|
|
12
|
+
export declare function OrganizationList({ hrefPattern, createOrganizationUrl, }: OrganizationListProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=OrganizationList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrganizationList.d.ts","sourceRoot":"","sources":["../../src/ui/OrganizationList.tsx"],"names":[],"mappings":"AAUA,MAAM,MAAM,qBAAqB,GAAG;IAClC;iCAC6B;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,WAA2B,EAC3B,qBAA8C,GAC/C,EAAE,qBAAqB,2CA4EvB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useAuthfyio } from '../provider.js';
|
|
5
|
+
import { useAuth } from '../hooks.js';
|
|
6
|
+
import { ensureStylesInjected } from './styles.js';
|
|
7
|
+
/**
|
|
8
|
+
* List view of every organization the signed-in user belongs to. Useful
|
|
9
|
+
* as a full-page picker after login.
|
|
10
|
+
*/
|
|
11
|
+
export function OrganizationList({ hrefPattern = '/org/{slug}', createOrganizationUrl = '/create-organization', }) {
|
|
12
|
+
useEffect(ensureStylesInjected, []);
|
|
13
|
+
const { client } = useAuthfyio();
|
|
14
|
+
const baseUrl = client.baseUrl;
|
|
15
|
+
const auth = useAuth();
|
|
16
|
+
const [orgs, setOrgs] = useState(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!auth.isSignedIn) {
|
|
19
|
+
setOrgs([]);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
fetch(`${baseUrl}/v1/orgs`, { credentials: 'include' })
|
|
23
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
24
|
+
.then((body) => setOrgs(body?.orgs ?? []))
|
|
25
|
+
.catch(() => setOrgs([]));
|
|
26
|
+
}, [auth.isSignedIn, baseUrl]);
|
|
27
|
+
const loading = orgs === null;
|
|
28
|
+
async function selectOrg(id) {
|
|
29
|
+
await fetch(`${baseUrl}/v1/orgs/current`, {
|
|
30
|
+
method: 'PATCH',
|
|
31
|
+
credentials: 'include',
|
|
32
|
+
headers: { 'content-type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ orgId: id }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return (_jsxs("div", { className: "af-card", style: { maxWidth: 520 }, children: [_jsx("h1", { className: "af-title", children: "Your organizations" }), _jsx("p", { className: "af-subtitle", children: "Pick one to work in, or create a new one." }), loading && _jsx("div", { className: "af-subtitle", children: "Loading\u2026" }), !loading && orgs.length === 0 && (_jsx("div", { className: "af-subtitle", children: "You're not a member of any organizations yet." })), _jsx("ul", { style: { listStyle: 'none', margin: 0, padding: 0 }, children: orgs?.map((o) => {
|
|
37
|
+
const href = hrefPattern.replace('{slug}', o.slug).replace('{id}', o.id);
|
|
38
|
+
return (_jsx("li", { style: { borderTop: '1px solid rgba(17,24,39,0.06)' }, children: _jsxs("a", { href: href, onClick: () => selectOrg(o.id), style: {
|
|
39
|
+
display: 'flex',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
gap: 12,
|
|
42
|
+
padding: '12px 4px',
|
|
43
|
+
textDecoration: 'none',
|
|
44
|
+
color: '#111827',
|
|
45
|
+
}, children: [_jsx("span", { className: "af-avatar", "aria-hidden": true, children: (o.name[0] ?? '?').toUpperCase() }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontSize: 13.5, fontWeight: 600 }, children: o.name }), _jsxs("div", { style: { fontSize: 11.5, color: '#6b7280' }, children: ["/", o.slug] })] }), _jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: _jsx("path", { d: "M5 12h14M13 5l7 7-7 7" }) })] }) }, o.id));
|
|
46
|
+
}) }), _jsx("div", { className: "af-primary-row", children: _jsx("a", { href: createOrganizationUrl, className: "af-btn af-btn-primary", style: { textDecoration: 'none' }, children: "Create organization" }) })] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Tab = 'general' | 'members' | 'invitations';
|
|
2
|
+
export type OrganizationProfileProps = {
|
|
3
|
+
initialTab?: Tab;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Full organization settings panel — general info, members list, pending
|
|
7
|
+
* invitations. Requires an
|
|
8
|
+
* active organization; renders a stub when none is selected.
|
|
9
|
+
*/
|
|
10
|
+
export declare function OrganizationProfile({ initialTab }: OrganizationProfileProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=OrganizationProfile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrganizationProfile.d.ts","sourceRoot":"","sources":["../../src/ui/OrganizationProfile.tsx"],"names":[],"mappings":"AAWA,KAAK,GAAG,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;AAEjD,MAAM,MAAM,wBAAwB,GAAG;IACrC,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,UAAsB,EAAE,EAAE,wBAAwB,2CAiDvF"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useAuthfyio } from '../provider.js';
|
|
5
|
+
import { useAuth, useOrganization } from '../hooks.js';
|
|
6
|
+
import { ensureStylesInjected } from './styles.js';
|
|
7
|
+
/**
|
|
8
|
+
* Full organization settings panel — general info, members list, pending
|
|
9
|
+
* invitations. Requires an
|
|
10
|
+
* active organization; renders a stub when none is selected.
|
|
11
|
+
*/
|
|
12
|
+
export function OrganizationProfile({ initialTab = 'general' }) {
|
|
13
|
+
useEffect(ensureStylesInjected, []);
|
|
14
|
+
const { client } = useAuthfyio();
|
|
15
|
+
const baseUrl = client.baseUrl;
|
|
16
|
+
const auth = useAuth();
|
|
17
|
+
const { organization, isLoaded } = useOrganization();
|
|
18
|
+
const [tab, setTab] = useState(initialTab);
|
|
19
|
+
if (!auth.isSignedIn) {
|
|
20
|
+
return (_jsx("div", { className: "af-card", children: _jsx("div", { className: "af-error", children: "You must be signed in." }) }));
|
|
21
|
+
}
|
|
22
|
+
if (!isLoaded) {
|
|
23
|
+
return (_jsx("div", { className: "af-card", children: _jsx("div", { className: "af-subtitle", children: "Loading\u2026" }) }));
|
|
24
|
+
}
|
|
25
|
+
if (!organization) {
|
|
26
|
+
return (_jsxs("div", { className: "af-card", children: [_jsx("h1", { className: "af-title", children: "No active organization" }), _jsx("p", { className: "af-subtitle", children: "Switch to an organization first." })] }));
|
|
27
|
+
}
|
|
28
|
+
return (_jsxs("div", { className: "af-card", style: { maxWidth: 720 }, children: [_jsx("h1", { className: "af-title", children: organization.name }), _jsxs("p", { className: "af-subtitle", children: ["/", organization.slug] }), _jsxs("div", { className: "af-profile", children: [_jsxs("div", { className: "af-profile-nav", role: "tablist", children: [_jsx("button", { role: "tab", className: `af-profile-nav-item${tab === 'general' ? ' is-active' : ''}`, onClick: () => setTab('general'), children: "General" }), _jsx("button", { role: "tab", className: `af-profile-nav-item${tab === 'members' ? ' is-active' : ''}`, onClick: () => setTab('members'), children: "Members" }), _jsx("button", { role: "tab", className: `af-profile-nav-item${tab === 'invitations' ? ' is-active' : ''}`, onClick: () => setTab('invitations'), children: "Invitations" })] }), _jsxs("div", { className: "af-profile-panel", role: "tabpanel", children: [tab === 'general' && _jsx(GeneralTab, { org: organization }), tab === 'members' && _jsx(MembersTab, { baseUrl: baseUrl, orgId: organization.id }), tab === 'invitations' && _jsx(InvitationsTab, { baseUrl: baseUrl, orgId: organization.id, role: auth.orgRole ?? 'member' })] })] })] }));
|
|
29
|
+
}
|
|
30
|
+
function GeneralTab({ org }) {
|
|
31
|
+
return (_jsxs("div", { children: [_jsx("h3", { children: "General" }), _jsx("p", { className: "af-sub", children: "Public identity for this organization." }), _jsxs("div", { className: "af-row", children: [_jsx("span", { className: "af-row-key", children: "Organization ID" }), _jsx("span", { className: "af-row-val", style: { fontFamily: 'monospace', fontSize: 12 }, children: org.id })] }), _jsxs("div", { className: "af-row", children: [_jsx("span", { className: "af-row-key", children: "Name" }), _jsx("span", { className: "af-row-val", children: org.name })] }), _jsxs("div", { className: "af-row", children: [_jsx("span", { className: "af-row-key", children: "Slug" }), _jsxs("span", { className: "af-row-val", children: ["/", org.slug] })] })] }));
|
|
32
|
+
}
|
|
33
|
+
function MembersTab({ baseUrl, orgId }) {
|
|
34
|
+
const [members, setMembers] = useState(null);
|
|
35
|
+
const [error, setError] = useState(null);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
let cancelled = false;
|
|
38
|
+
(async () => {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`${baseUrl}/v1/orgs/${encodeURIComponent(orgId)}/members`, {
|
|
41
|
+
credentials: 'include',
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
if (!cancelled) {
|
|
45
|
+
setError(res.status === 400 ? 'You are not a member of this organization.' : 'Could not load members.');
|
|
46
|
+
setMembers([]);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const body = (await res.json());
|
|
51
|
+
if (!cancelled)
|
|
52
|
+
setMembers(body.members ?? []);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
if (!cancelled) {
|
|
56
|
+
setError('Could not load members.');
|
|
57
|
+
setMembers([]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
return () => { cancelled = true; };
|
|
62
|
+
}, [baseUrl, orgId]);
|
|
63
|
+
return (_jsxs("div", { children: [_jsx("h3", { children: "Members" }), _jsx("p", { className: "af-sub", children: "People with access to this organization." }), error && _jsx("div", { className: "af-error", children: error }), members === null && _jsx("div", { className: "af-subtitle", children: "Loading\u2026" }), members && members.length === 0 && !error && (_jsx("div", { style: { fontSize: 13, color: '#6b7280' }, children: "No members yet." })), members && members.length > 0 && (_jsx("ul", { style: { listStyle: 'none', padding: 0, margin: 0 }, children: members.map((m) => (_jsxs("li", { className: "af-row", style: { padding: '10px 0' }, children: [_jsx("span", { className: "af-row-val", style: { flex: 1, textAlign: 'left' }, children: m.email ?? _jsxs("span", { style: { color: '#9ca3af', fontFamily: 'monospace' }, children: [m.userId.slice(0, 8), "\u2026"] }) }), _jsx("span", { className: "af-pill af-pill-muted", children: m.role })] }, m.userId))) }))] }));
|
|
64
|
+
}
|
|
65
|
+
function InvitationsTab({ baseUrl, orgId, role, }) {
|
|
66
|
+
const [invites, setInvites] = useState(null);
|
|
67
|
+
const [email, setEmail] = useState('');
|
|
68
|
+
const [sending, setSending] = useState(false);
|
|
69
|
+
const [error, setError] = useState(null);
|
|
70
|
+
const canInvite = role === 'admin' || role === 'owner';
|
|
71
|
+
async function refresh() {
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(`${baseUrl}/v1/orgs/${encodeURIComponent(orgId)}/invitations`, {
|
|
74
|
+
credentials: 'include',
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
setInvites([]);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const body = (await res.json());
|
|
81
|
+
setInvites(body.invitations ?? []);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
setInvites([]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
void refresh();
|
|
89
|
+
}, [baseUrl, orgId]);
|
|
90
|
+
async function invite(e) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
setSending(true);
|
|
93
|
+
setError(null);
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(`${baseUrl}/v1/orgs/${encodeURIComponent(orgId)}/invitations`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
credentials: 'include',
|
|
98
|
+
headers: { 'content-type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ email, role: 'member' }),
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const body = await res.json().catch(() => ({}));
|
|
103
|
+
throw new Error(body?.message ?? 'Could not invite.');
|
|
104
|
+
}
|
|
105
|
+
setEmail('');
|
|
106
|
+
await refresh();
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
setError(err?.message ?? 'Could not invite.');
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
setSending(false);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return (_jsxs("div", { children: [_jsx("h3", { children: "Invitations" }), _jsx("p", { className: "af-sub", children: "Pending member invitations." }), canInvite && (_jsxs("form", { onSubmit: invite, style: { display: 'flex', gap: 8, marginBottom: 16 }, children: [_jsx("input", { className: "af-input", type: "email", required: true, value: email, onChange: (e) => setEmail(e.target.value), placeholder: "teammate@example.com", style: { flex: 1 } }), _jsx("button", { type: "submit", className: "af-btn af-btn-primary", disabled: sending, style: { width: 'auto' }, children: sending ? 'Sending…' : 'Invite' })] })), error && _jsx("div", { className: "af-error", children: error }), invites === null && _jsx("div", { className: "af-subtitle", children: "Loading\u2026" }), invites && invites.length === 0 && (_jsx("div", { style: { fontSize: 13, color: '#6b7280' }, children: "No pending invitations." })), invites && invites.length > 0 && (_jsx("ul", { style: { listStyle: 'none', padding: 0, margin: 0 }, children: invites.map((inv) => (_jsxs("li", { className: "af-row", style: { padding: '10px 0' }, children: [_jsx("span", { className: "af-row-val", style: { flex: 1, textAlign: 'left' }, children: inv.email }), _jsx("span", { className: "af-pill af-pill-muted", children: inv.role })] }, inv.id))) }))] }));
|
|
116
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type OrganizationSwitcherProps = {
|
|
2
|
+
/** Path users navigate to when they click "Create organization". Default '/create-organization'. */
|
|
3
|
+
createOrganizationUrl?: string;
|
|
4
|
+
/** Optional callback after a successful switch (before the page reloads). */
|
|
5
|
+
afterSelectOrganization?: (orgId: string) => void;
|
|
6
|
+
/** Where to land after a switch. Default: reload current URL. */
|
|
7
|
+
redirectUrl?: string;
|
|
8
|
+
/** Hide the personal ("no org") entry. Default false — */
|
|
9
|
+
hidePersonal?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Active-organization picker. Lists the signed-in user's orgs, lets them
|
|
13
|
+
* switch, and links to create a new one.
|
|
14
|
+
* Invisible when the user is signed out — wrap in `<SignedIn>`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function OrganizationSwitcher({ createOrganizationUrl, afterSelectOrganization, redirectUrl, hidePersonal, }: OrganizationSwitcherProps): import("react/jsx-runtime").JSX.Element | null;
|
|
17
|
+
//# sourceMappingURL=OrganizationSwitcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrganizationSwitcher.d.ts","sourceRoot":"","sources":["../../src/ui/OrganizationSwitcher.tsx"],"names":[],"mappings":"AAUA,MAAM,MAAM,yBAAyB,GAAG;IACtC,oGAAoG;IACpG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,qBAA8C,EAC9C,uBAAuB,EACvB,WAAW,EACX,YAAoB,GACrB,EAAE,yBAAyB,kDA4G3B"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useAuthfyio } from '../provider.js';
|
|
5
|
+
import { useAuth } from '../hooks.js';
|
|
6
|
+
import { ensureStylesInjected } from './styles.js';
|
|
7
|
+
/**
|
|
8
|
+
* Active-organization picker. Lists the signed-in user's orgs, lets them
|
|
9
|
+
* switch, and links to create a new one.
|
|
10
|
+
* Invisible when the user is signed out — wrap in `<SignedIn>`.
|
|
11
|
+
*/
|
|
12
|
+
export function OrganizationSwitcher({ createOrganizationUrl = '/create-organization', afterSelectOrganization, redirectUrl, hidePersonal = false, }) {
|
|
13
|
+
useEffect(ensureStylesInjected, []);
|
|
14
|
+
const { client } = useAuthfyio();
|
|
15
|
+
const baseUrl = client.baseUrl;
|
|
16
|
+
const auth = useAuth();
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
const [orgs, setOrgs] = useState([]);
|
|
19
|
+
const rootRef = useRef(null);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!auth.isSignedIn)
|
|
22
|
+
return;
|
|
23
|
+
fetch(`${baseUrl}/v1/orgs`, { credentials: 'include' })
|
|
24
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
25
|
+
.then((body) => {
|
|
26
|
+
if (body && Array.isArray(body.orgs))
|
|
27
|
+
setOrgs(body.orgs);
|
|
28
|
+
})
|
|
29
|
+
.catch(() => { });
|
|
30
|
+
}, [auth.isSignedIn, baseUrl]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!open)
|
|
33
|
+
return;
|
|
34
|
+
function onDocClick(e) {
|
|
35
|
+
if (!rootRef.current?.contains(e.target))
|
|
36
|
+
setOpen(false);
|
|
37
|
+
}
|
|
38
|
+
window.addEventListener('mousedown', onDocClick);
|
|
39
|
+
return () => window.removeEventListener('mousedown', onDocClick);
|
|
40
|
+
}, [open]);
|
|
41
|
+
async function selectOrg(orgId) {
|
|
42
|
+
await fetch(`${baseUrl}/v1/orgs/current`, {
|
|
43
|
+
method: 'PATCH',
|
|
44
|
+
credentials: 'include',
|
|
45
|
+
headers: { 'content-type': 'application/json' },
|
|
46
|
+
body: JSON.stringify({ orgId }),
|
|
47
|
+
});
|
|
48
|
+
if (afterSelectOrganization && orgId)
|
|
49
|
+
afterSelectOrganization(orgId);
|
|
50
|
+
if (typeof window !== 'undefined') {
|
|
51
|
+
window.location.href = redirectUrl ?? window.location.pathname;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!auth.isSignedIn)
|
|
55
|
+
return null;
|
|
56
|
+
const activeOrg = orgs.find((o) => o.id === auth.orgId);
|
|
57
|
+
const activeLabel = activeOrg?.name ?? (hidePersonal ? 'Select organization' : 'Personal account');
|
|
58
|
+
return (_jsxs("div", { ref: rootRef, style: { position: 'relative', display: 'inline-block' }, children: [_jsxs("button", { type: "button", className: "af-user-btn", "aria-haspopup": "menu", "aria-expanded": open, onClick: () => setOpen((v) => !v), children: [_jsx("span", { className: "af-avatar", "aria-hidden": true, style: { background: 'linear-gradient(135deg,#4f3df0,#5b5bd6)', color: '#fff' }, children: (activeLabel[0] ?? '?').toUpperCase() }), _jsx("span", { children: activeLabel }), _jsx("svg", { width: "11", height: "11", viewBox: "0 0 16 16", fill: "none", "aria-hidden": true, children: _jsx("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), open && (_jsxs("div", { className: "af-menu", role: "menu", style: { minWidth: 260 }, children: [_jsx("div", { style: { padding: '6px 8px 10px', fontSize: 10.5, fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#9ca3af' }, children: "Switch organization" }), !hidePersonal && (_jsxs("button", { type: "button", className: "af-menu-item", role: "menuitem", onClick: () => selectOrg(null), style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx("span", { className: "af-avatar af-avatar-sm", "aria-hidden": true, children: "P" }), "Personal account", !auth.orgId && _jsx("span", { style: { marginLeft: 'auto', fontSize: 10.5, color: '#4f3df0' }, children: "Active" })] })), orgs.map((o) => (_jsxs("button", { type: "button", className: "af-menu-item", role: "menuitem", onClick: () => selectOrg(o.id), style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx("span", { className: "af-avatar af-avatar-sm", "aria-hidden": true, children: o.name[0]?.toUpperCase() ?? '?' }), _jsx("span", { style: { minWidth: 0, flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis' }, children: o.name }), o.id === auth.orgId && _jsx("span", { style: { fontSize: 10.5, color: '#4f3df0' }, children: "Active" })] }, o.id))), orgs.length === 0 && (_jsx("div", { style: { padding: '8px 10px', fontSize: 12, color: '#6b7280' }, children: "You're not in any organizations yet." })), _jsx("div", { className: "af-menu-sep" }), _jsx("a", { href: createOrganizationUrl, className: "af-menu-item", role: "menuitem", children: "+ Create organization" })] }))] }));
|
|
59
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type PricingTableProps = {
|
|
2
|
+
/** Only show these plan names (case-insensitive). Default: all. */
|
|
3
|
+
includePlans?: string[];
|
|
4
|
+
/** Which billing cycle to default to. */
|
|
5
|
+
defaultBillingCycle?: 'monthly' | 'annual';
|
|
6
|
+
/** Label override for the primary CTA. Default "Subscribe". */
|
|
7
|
+
ctaLabel?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Renders every SubscriptionPlanEntity the instance exposes as a cards grid.
|
|
11
|
+
* Clicking a card triggers `useCheckout().startCheckout(planId, cycle)` which
|
|
12
|
+
* redirects the browser to Stripe Checkout.
|
|
13
|
+
*/
|
|
14
|
+
export declare function PricingTable({ includePlans, defaultBillingCycle, ctaLabel, }: PricingTableProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=PricingTable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PricingTable.d.ts","sourceRoot":"","sources":["../../src/ui/PricingTable.tsx"],"names":[],"mappings":"AAgBA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,yCAAyC;IACzC,mBAAmB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC3C,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,YAAY,EACZ,mBAA+B,EAC/B,QAAsB,GACvB,EAAE,iBAAiB,2CAkHnB"}
|