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.
Files changed (68) hide show
  1. package/README.md +83 -0
  2. package/dist/client.d.ts +128 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +227 -0
  5. package/dist/components.d.ts +79 -0
  6. package/dist/components.d.ts.map +1 -0
  7. package/dist/components.js +111 -0
  8. package/dist/hooks-flow.d.ts +59 -0
  9. package/dist/hooks-flow.d.ts.map +1 -0
  10. package/dist/hooks-flow.js +133 -0
  11. package/dist/hooks.d.ts +113 -0
  12. package/dist/hooks.d.ts.map +1 -0
  13. package/dist/hooks.js +212 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +15 -0
  17. package/dist/jwt.d.ts +20 -0
  18. package/dist/jwt.d.ts.map +1 -0
  19. package/dist/jwt.js +30 -0
  20. package/dist/provider.d.ts +50 -0
  21. package/dist/provider.d.ts.map +1 -0
  22. package/dist/provider.js +91 -0
  23. package/dist/ui/CheckoutButton.d.ts +14 -0
  24. package/dist/ui/CheckoutButton.d.ts.map +1 -0
  25. package/dist/ui/CheckoutButton.js +28 -0
  26. package/dist/ui/Control.d.ts +17 -0
  27. package/dist/ui/Control.d.ts.map +1 -0
  28. package/dist/ui/Control.js +33 -0
  29. package/dist/ui/CreateOrganization.d.ts +19 -0
  30. package/dist/ui/CreateOrganization.d.ts.map +1 -0
  31. package/dist/ui/CreateOrganization.js +75 -0
  32. package/dist/ui/OrganizationList.d.ts +13 -0
  33. package/dist/ui/OrganizationList.d.ts.map +1 -0
  34. package/dist/ui/OrganizationList.js +47 -0
  35. package/dist/ui/OrganizationProfile.d.ts +12 -0
  36. package/dist/ui/OrganizationProfile.d.ts.map +1 -0
  37. package/dist/ui/OrganizationProfile.js +116 -0
  38. package/dist/ui/OrganizationSwitcher.d.ts +17 -0
  39. package/dist/ui/OrganizationSwitcher.d.ts.map +1 -0
  40. package/dist/ui/OrganizationSwitcher.js +59 -0
  41. package/dist/ui/PricingTable.d.ts +15 -0
  42. package/dist/ui/PricingTable.d.ts.map +1 -0
  43. package/dist/ui/PricingTable.js +74 -0
  44. package/dist/ui/SignIn.d.ts +23 -0
  45. package/dist/ui/SignIn.d.ts.map +1 -0
  46. package/dist/ui/SignIn.js +489 -0
  47. package/dist/ui/SignUp.d.ts +18 -0
  48. package/dist/ui/SignUp.d.ts.map +1 -0
  49. package/dist/ui/SignUp.js +153 -0
  50. package/dist/ui/UnstyledButtons.d.ts +24 -0
  51. package/dist/ui/UnstyledButtons.d.ts.map +1 -0
  52. package/dist/ui/UnstyledButtons.js +42 -0
  53. package/dist/ui/UserAvatar.d.ts +14 -0
  54. package/dist/ui/UserAvatar.d.ts.map +1 -0
  55. package/dist/ui/UserAvatar.js +52 -0
  56. package/dist/ui/UserButton.d.ts +15 -0
  57. package/dist/ui/UserButton.d.ts.map +1 -0
  58. package/dist/ui/UserButton.js +82 -0
  59. package/dist/ui/UserProfile.d.ts +19 -0
  60. package/dist/ui/UserProfile.d.ts.map +1 -0
  61. package/dist/ui/UserProfile.js +199 -0
  62. package/dist/ui/index.d.ts +14 -0
  63. package/dist/ui/index.d.ts.map +1 -0
  64. package/dist/ui/index.js +13 -0
  65. package/dist/ui/styles.d.ts +10 -0
  66. package/dist/ui/styles.d.ts.map +1 -0
  67. package/dist/ui/styles.js +291 -0
  68. package/package.json +46 -0
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { useAuthfyio } from '../provider.js';
5
+ import { ensureStylesInjected } from './styles.js';
6
+ const SOCIAL_LABELS = {
7
+ google: 'Google',
8
+ github: 'GitHub',
9
+ facebook: 'Facebook',
10
+ apple: 'Apple',
11
+ microsoft: 'Microsoft',
12
+ linkedin: 'LinkedIn',
13
+ discord: 'Discord',
14
+ slack: 'Slack',
15
+ twitter: 'X / Twitter',
16
+ };
17
+ const WALLET_LABELS = {
18
+ metamask: 'MetaMask',
19
+ coinbase_wallet: 'Coinbase Wallet',
20
+ okx_wallet: 'OKX Wallet',
21
+ solana: 'Solana',
22
+ base: 'Base',
23
+ };
24
+ const WALLET_COLORS = {
25
+ metamask: '#F6851B',
26
+ coinbase_wallet: '#0052FF',
27
+ okx_wallet: '#000000',
28
+ solana: '#9945FF',
29
+ base: '#0052FF',
30
+ };
31
+ export function SignUp({ redirectUrl = '/', signInUrl = '/sign-in', title = 'Create your account', subtitle = 'One account for everything.', showBranding = true, brandText = 'Authfyio', socialProviders: socialOverride, onSignUp, }) {
32
+ useEffect(ensureStylesInjected, []);
33
+ const { client } = useAuthfyio();
34
+ const baseUrl = client.baseUrl;
35
+ const [config, setConfig] = useState(null);
36
+ useEffect(() => {
37
+ let cancelled = false;
38
+ client.fetchAuthConfig().then((c) => {
39
+ if (cancelled)
40
+ return;
41
+ setConfig(c);
42
+ });
43
+ return () => {
44
+ cancelled = true;
45
+ };
46
+ }, [client]);
47
+ // Mirror SignIn — pk + return_url query on every OAuth anchor so the
48
+ // top-level navigation lands cookies on this app's origin via the
49
+ // /api/af/oauth-finish bridge. See SignIn for the full rationale.
50
+ const oauthQuery = (() => {
51
+ if (typeof window === 'undefined')
52
+ return '';
53
+ const params = new URLSearchParams();
54
+ if (client.publishableKey)
55
+ params.set('publishable_key', client.publishableKey);
56
+ const finish = new URL('/api/af/oauth-finish', window.location.origin);
57
+ finish.searchParams.set('to', redirectUrl);
58
+ params.set('return_url', finish.toString());
59
+ return '?' + params.toString();
60
+ })();
61
+ const enabledSocial = (socialOverride ?? (config?.socialProviders ?? [])).filter((p) => SOCIAL_LABELS[p]);
62
+ const enabledWallets = (config?.web3Wallets ?? []).filter((w) => WALLET_LABELS[w]);
63
+ const legal = config?.legal;
64
+ const brand = config?.branding;
65
+ const cardStyle = brand?.primaryColor
66
+ ? { ['--af-primary']: brand.primaryColor }
67
+ : {};
68
+ const effectiveBrandText = brand?.appName ?? brandText;
69
+ const showPoweredBy = brand && !brand.removeBranding;
70
+ const su = config?.signUp;
71
+ const signUpEnabled = su?.enabled !== false;
72
+ // What fields/identifiers the form actually needs based on the
73
+ // dashboard config. We need at least one identifier (email, username,
74
+ // or phone) plus a credential (password) — otherwise there's nothing
75
+ // to submit.
76
+ const showEmail = !!su?.email;
77
+ const showUsername = !!su?.username;
78
+ const usernameRequired = !!su?.username?.required;
79
+ const showPhone = !!su?.phone;
80
+ const showPassword = !!su?.password;
81
+ const showFirstAndLastName = !!su?.firstAndLastName;
82
+ const requireFirstAndLastName = !!su?.firstAndLastName?.required;
83
+ const hasSomething = showEmail || showUsername || showPhone || enabledSocial.length > 0 || enabledWallets.length > 0;
84
+ const [email, setEmail] = useState('');
85
+ const [username, setUsername] = useState('');
86
+ const [phone, setPhone] = useState('');
87
+ const [password, setPassword] = useState('');
88
+ const [firstName, setFirstName] = useState('');
89
+ const [lastName, setLastName] = useState('');
90
+ const [loading, setLoading] = useState(false);
91
+ const [error, setError] = useState(null);
92
+ async function submit(e) {
93
+ e.preventDefault();
94
+ setLoading(true);
95
+ setError(null);
96
+ try {
97
+ const body = {};
98
+ if (showEmail && email)
99
+ body.email = email;
100
+ if (showUsername && username)
101
+ body.username = username;
102
+ if (showPhone && phone)
103
+ body.phone = phone;
104
+ if (showPassword && password)
105
+ body.password = password;
106
+ if (showFirstAndLastName && firstName)
107
+ body.firstName = firstName;
108
+ if (showFirstAndLastName && lastName)
109
+ body.lastName = lastName;
110
+ // Use email-password endpoint when there's an email; otherwise the
111
+ // matching identifier-specific endpoint. (Username/phone-only
112
+ // sign-up endpoints are 0.3 work — for now require email or
113
+ // social.)
114
+ const endpoint = showEmail
115
+ ? '/v1/auth/sign-up/email-password'
116
+ : showUsername
117
+ ? '/v1/auth/sign-up/username-password'
118
+ : '/v1/auth/sign-up/email-password';
119
+ const res = await fetch(`${baseUrl}${endpoint}`, {
120
+ method: 'POST',
121
+ credentials: 'include',
122
+ headers: { 'content-type': 'application/json' },
123
+ body: JSON.stringify(body),
124
+ });
125
+ if (!res.ok) {
126
+ const b = await res.json().catch(() => ({}));
127
+ throw new Error(b?.message ?? 'Sign-up failed.');
128
+ }
129
+ const ok = (await res.json());
130
+ if (onSignUp)
131
+ onSignUp(ok);
132
+ if (typeof window !== 'undefined')
133
+ window.location.href = redirectUrl;
134
+ }
135
+ catch (err) {
136
+ setError(err?.message ?? 'Sign-up failed.');
137
+ }
138
+ finally {
139
+ setLoading(false);
140
+ }
141
+ }
142
+ return (_jsxs("div", { className: "af-card", role: "form", "aria-label": "Sign up", style: cardStyle, children: [showBranding && (_jsxs("div", { className: "af-brand", children: [brand?.logoUrl ? (_jsx("img", { src: brand.logoUrl, alt: "", style: { width: 26, height: 26, borderRadius: 8, objectFit: 'cover' } })) : (_jsx("span", { className: "af-brand-mark", "aria-hidden": true })), _jsx("span", { className: "af-brand-text", children: effectiveBrandText })] })), _jsx("h1", { className: "af-title", children: title }), _jsx("p", { className: "af-subtitle", children: subtitle }), !signUpEnabled ? (_jsx("div", { className: "af-error", children: "Sign-up is disabled for this app. Ask the app owner for an invite." })) : (_jsxs(_Fragment, { children: [(enabledSocial.length > 0 || enabledWallets.length > 0) && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "af-social-grid", children: [enabledSocial.map((p) => (_jsxs("a", { className: "af-social-btn", href: `${baseUrl}/v1/auth/oauth/${p}/authorize${oauthQuery}`, "aria-label": `Sign up with ${SOCIAL_LABELS[p]}`, children: ["Continue with ", SOCIAL_LABELS[p]] }, p))), enabledWallets.map((w) => (_jsxs("a", { className: "af-social-btn", href: `${baseUrl}/v1/auth/web3/${w}/authorize${oauthQuery}`, "aria-label": `Sign up with ${WALLET_LABELS[w]}`, children: [_jsx("span", { className: "af-social-icon", "aria-hidden": true, style: {
143
+ display: 'inline-flex',
144
+ alignItems: 'center',
145
+ justifyContent: 'center',
146
+ background: WALLET_COLORS[w] ?? '#6b7280',
147
+ color: 'white',
148
+ borderRadius: 4,
149
+ fontSize: 9,
150
+ fontWeight: 700,
151
+ textTransform: 'uppercase',
152
+ }, children: (WALLET_LABELS[w] ?? w).slice(0, 1) }), "Continue with ", WALLET_LABELS[w]] }, w)))] }), (showEmail || showUsername || showPhone) && _jsx("div", { className: "af-divider", children: "or" })] })), (showEmail || showUsername || showPhone) && (_jsxs("form", { onSubmit: submit, children: [showEmail && (_jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", htmlFor: "af-signup-email", children: "Email" }), _jsx("input", { id: "af-signup-email", className: "af-input", type: "email", autoComplete: "email", required: su?.emailRequired ?? true, value: email, onChange: (e) => setEmail(e.target.value), placeholder: "you@company.com" })] })), showUsername && (_jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", htmlFor: "af-signup-username", children: "Username" }), _jsx("input", { id: "af-signup-username", className: "af-input", type: "text", autoComplete: "username", required: usernameRequired, minLength: su?.username?.minLength ?? 4, maxLength: su?.username?.maxLength ?? 64, value: username, onChange: (e) => setUsername(e.target.value), placeholder: "yourname" })] })), showPhone && (_jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", htmlFor: "af-signup-phone", children: "Phone" }), _jsx("input", { id: "af-signup-phone", className: "af-input", type: "tel", autoComplete: "tel", value: phone, onChange: (e) => setPhone(e.target.value), placeholder: "+14155551234" })] })), showFirstAndLastName && (_jsxs("div", { style: { display: 'flex', gap: 8 }, children: [_jsxs("div", { className: "af-field", style: { flex: 1 }, children: [_jsx("label", { className: "af-label", htmlFor: "af-signup-first", children: "First name" }), _jsx("input", { id: "af-signup-first", className: "af-input", type: "text", autoComplete: "given-name", required: requireFirstAndLastName, value: firstName, onChange: (e) => setFirstName(e.target.value) })] }), _jsxs("div", { className: "af-field", style: { flex: 1 }, children: [_jsx("label", { className: "af-label", htmlFor: "af-signup-last", children: "Last name" }), _jsx("input", { id: "af-signup-last", className: "af-input", type: "text", autoComplete: "family-name", required: requireFirstAndLastName, value: lastName, onChange: (e) => setLastName(e.target.value) })] })] })), showPassword && (_jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", htmlFor: "af-signup-password", children: "Password" }), _jsx("input", { id: "af-signup-password", className: "af-input", type: "password", autoComplete: "new-password", required: true, minLength: 8, value: password, onChange: (e) => setPassword(e.target.value), placeholder: "At least 8 characters" })] })), _jsx("div", { className: "af-primary-row", children: _jsx("button", { type: "submit", className: "af-btn af-btn-primary", disabled: loading, children: loading ? 'Creating account…' : 'Create account' }) })] })), config && !hasSomething && (_jsx("div", { className: "af-error", children: "No sign-up methods are enabled for this app. Enable at least one identifier (email, username, phone, or a social provider) in the Authfyio dashboard." }))] })), error && _jsx("div", { className: "af-error", children: error }), (legal?.termsOfServiceUrl || legal?.privacyPolicyUrl) && (_jsxs("div", { className: "af-legal", children: ["By creating an account you agree to our", ' ', legal?.termsOfServiceUrl && (_jsxs(_Fragment, { children: [_jsx("a", { href: legal.termsOfServiceUrl, target: "_blank", rel: "noreferrer", children: "Terms" }), legal?.privacyPolicyUrl ? ' and ' : '.'] })), legal?.privacyPolicyUrl && (_jsxs(_Fragment, { children: [_jsx("a", { href: legal.privacyPolicyUrl, target: "_blank", rel: "noreferrer", children: "Privacy Policy" }), "."] }))] })), signUpEnabled && (_jsxs("div", { className: "af-footer", children: ["Already have an account? ", _jsx("a", { href: signInUrl, children: "Sign in" })] })), showPoweredBy && (_jsxs("div", { className: "af-powered", children: ["Powered by ", _jsx("a", { href: "https://authfyio.com", target: "_blank", rel: "noreferrer", children: "Authfyio" })] }))] }));
153
+ }
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ /**
3
+ * Unstyled button wrappers — they inherit your app's own styling. Use them
4
+ * when you want sign-in / sign-up / sign-out behavior without the prebuilt
5
+ * card UI: <SignInButton />, <SignUpButton />, <SignOutButton />.
6
+ */
7
+ type ButtonProps = {
8
+ children?: React.ReactNode;
9
+ className?: string;
10
+ /** Path to your own sign-in / sign-up page. Default '/sign-in' / '/sign-up'. */
11
+ url?: string;
12
+ /** Preserve current URL as a redirect_url query param (default true). */
13
+ preserveReturnPath?: boolean;
14
+ };
15
+ export declare function SignInButton({ children, className, url, preserveReturnPath }: ButtonProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function SignUpButton({ children, className, url, preserveReturnPath }: ButtonProps): import("react/jsx-runtime").JSX.Element;
17
+ type SignOutProps = {
18
+ children?: React.ReactNode;
19
+ className?: string;
20
+ redirectUrl?: string;
21
+ };
22
+ export declare function UnstyledSignOutButton({ children, className, redirectUrl }: SignOutProps): import("react/jsx-runtime").JSX.Element;
23
+ export {};
24
+ //# sourceMappingURL=UnstyledButtons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UnstyledButtons.d.ts","sourceRoot":"","sources":["../../src/ui/UnstyledButtons.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B;;;;GAIG;AAEH,KAAK,WAAW,GAAG;IACjB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAgB,EAAE,kBAAyB,EAAE,EAAE,WAAW,2CAU7G;AAED,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAgB,EAAE,kBAAyB,EAAE,EAAE,WAAW,2CAU7G;AAED,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAiB,EAAE,EAAE,YAAY,2CAe7F"}
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useAuthfyio } from '../provider.js';
4
+ export function SignInButton({ children, className, url = '/sign-in', preserveReturnPath = true }) {
5
+ return (_jsx("a", { href: buildHref(url, preserveReturnPath), className: className, style: className ? undefined : inlineLinkStyle, children: children ?? 'Sign in' }));
6
+ }
7
+ export function SignUpButton({ children, className, url = '/sign-up', preserveReturnPath = true }) {
8
+ return (_jsx("a", { href: buildHref(url, preserveReturnPath), className: className, style: className ? undefined : inlineLinkStyle, children: children ?? 'Sign up' }));
9
+ }
10
+ export function UnstyledSignOutButton({ children, className, redirectUrl = '/' }) {
11
+ const { client } = useAuthfyio();
12
+ const baseUrl = client.baseUrl;
13
+ async function onClick() {
14
+ try {
15
+ await fetch(`${baseUrl}/v1/auth/sign-out`, { method: 'POST', credentials: 'include' });
16
+ }
17
+ finally {
18
+ if (typeof window !== 'undefined')
19
+ window.location.href = redirectUrl;
20
+ }
21
+ }
22
+ return (_jsx("button", { type: "button", onClick: onClick, className: className, style: className ? undefined : inlineButtonStyle, children: children ?? 'Sign out' }));
23
+ }
24
+ function buildHref(path, preserve) {
25
+ if (!preserve || typeof window === 'undefined')
26
+ return path;
27
+ const ret = window.location.pathname + window.location.search;
28
+ if (!ret || ret === '/')
29
+ return path;
30
+ const sep = path.includes('?') ? '&' : '?';
31
+ return `${path}${sep}redirect_url=${encodeURIComponent(ret)}`;
32
+ }
33
+ const inlineLinkStyle = { color: 'inherit', textDecoration: 'underline' };
34
+ const inlineButtonStyle = {
35
+ font: 'inherit',
36
+ background: 'transparent',
37
+ border: 0,
38
+ padding: 0,
39
+ cursor: 'pointer',
40
+ color: 'inherit',
41
+ textDecoration: 'underline',
42
+ };
@@ -0,0 +1,14 @@
1
+ export type UserAvatarProps = {
2
+ /** Avatar edge length in px. Default 28. */
3
+ size?: number;
4
+ className?: string;
5
+ /** Render only the fallback initials (skip the Gravatar network call). */
6
+ disableImage?: boolean;
7
+ };
8
+ /**
9
+ * Standalone avatar — just the circle, no dropdown. Gravatar with an
10
+ * identicon fallback; renders the user's initials while the image is
11
+ * loading and when no image is available.
12
+ */
13
+ export declare function UserAvatar({ size, className, disableImage }: UserAvatarProps): import("react/jsx-runtime").JSX.Element;
14
+ //# sourceMappingURL=UserAvatar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserAvatar.d.ts","sourceRoot":"","sources":["../../src/ui/UserAvatar.tsx"],"names":[],"mappings":"AAOA,MAAM,MAAM,eAAe,GAAG;IAC5B,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,IAAS,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,eAAe,2CA0CjF"}
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect } from 'react';
4
+ import { useUser } from '../hooks.js';
5
+ import { ensureStylesInjected } from './styles.js';
6
+ /**
7
+ * Standalone avatar — just the circle, no dropdown. Gravatar with an
8
+ * identicon fallback; renders the user's initials while the image is
9
+ * loading and when no image is available.
10
+ */
11
+ export function UserAvatar({ size = 28, className, disableImage }) {
12
+ useEffect(ensureStylesInjected, []);
13
+ const { user } = useUser();
14
+ const email = user?.email ?? '';
15
+ const displayName = user?.firstName
16
+ ? `${user.firstName}${user.lastName ? ' ' + user.lastName : ''}`
17
+ : email.split('@')[0] || 'U';
18
+ const initials = getInitials(displayName, email);
19
+ const fontSize = Math.round(size * 0.4);
20
+ return (_jsxs("span", { className: className, style: {
21
+ width: size,
22
+ height: size,
23
+ borderRadius: '999px',
24
+ display: 'inline-flex',
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ overflow: 'hidden',
28
+ position: 'relative',
29
+ background: 'rgba(17,24,39,0.06)',
30
+ color: '#4b5563',
31
+ fontSize,
32
+ fontWeight: 600,
33
+ }, "aria-label": `Avatar for ${displayName}`, children: [!disableImage && email && (
34
+ // eslint-disable-next-line @next/next/no-img-element
35
+ _jsx("img", { src: gravatar(email, size * 2), alt: "", width: size, height: size, style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' } })), _jsx("span", { style: { position: 'relative' }, children: initials })] }));
36
+ }
37
+ function getInitials(name, email) {
38
+ const src = (name && name.trim()) || email || '?';
39
+ const parts = src.trim().split(/\s+/);
40
+ if (parts.length >= 2)
41
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
42
+ return parts[0].slice(0, 2).toUpperCase();
43
+ }
44
+ function gravatar(email, size) {
45
+ // Cheap stable hash — good enough for deterministic identicon fallbacks.
46
+ let h = 5381;
47
+ const norm = email.trim().toLowerCase();
48
+ for (let i = 0; i < norm.length; i++)
49
+ h = ((h << 5) + h + norm.charCodeAt(i)) | 0;
50
+ const hex = (h >>> 0).toString(16).padStart(8, '0');
51
+ return `https://www.gravatar.com/avatar/${hex.repeat(4)}?s=${size}&d=identicon`;
52
+ }
@@ -0,0 +1,15 @@
1
+ export type UserButtonProps = {
2
+ /** Redirect target after sign-out. Default '/'. */
3
+ afterSignOutUrl?: string;
4
+ /** Path to the user profile page (linked from the dropdown). Default '/user'. */
5
+ userProfileUrl?: string;
6
+ /** Show email under the name in the menu head. Default true. */
7
+ showEmail?: boolean;
8
+ };
9
+ /**
10
+ * Avatar button that opens a dropdown with "Manage account" and "Sign out".
11
+ * Invisible when signed out — wrap in
12
+ * <SignedIn> or guard yourself.
13
+ */
14
+ export declare function UserButton({ afterSignOutUrl, userProfileUrl, showEmail, }: UserButtonProps): import("react/jsx-runtime").JSX.Element | null;
15
+ //# sourceMappingURL=UserButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserButton.d.ts","sourceRoot":"","sources":["../../src/ui/UserButton.tsx"],"names":[],"mappings":"AAQA,MAAM,MAAM,eAAe,GAAG;IAC5B,mDAAmD;IACnD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iFAAiF;IACjF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACzB,eAAqB,EACrB,cAAwB,EACxB,SAAgB,GACjB,EAAE,eAAe,kDAyFjB"}
@@ -0,0 +1,82 @@
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, useUser } from '../hooks.js';
6
+ import { ensureStylesInjected } from './styles.js';
7
+ /**
8
+ * Avatar button that opens a dropdown with "Manage account" and "Sign out".
9
+ * Invisible when signed out — wrap in
10
+ * <SignedIn> or guard yourself.
11
+ */
12
+ export function UserButton({ afterSignOutUrl = '/', userProfileUrl = '/user', showEmail = true, }) {
13
+ useEffect(ensureStylesInjected, []);
14
+ const { client } = useAuthfyio();
15
+ const baseUrl = client.baseUrl;
16
+ const auth = useAuth();
17
+ const { user } = useUser();
18
+ const [open, setOpen] = useState(false);
19
+ const rootRef = useRef(null);
20
+ useEffect(() => {
21
+ if (!open)
22
+ return;
23
+ function onDocClick(e) {
24
+ if (!rootRef.current)
25
+ return;
26
+ if (!rootRef.current.contains(e.target))
27
+ setOpen(false);
28
+ }
29
+ function onKey(e) {
30
+ if (e.key === 'Escape')
31
+ setOpen(false);
32
+ }
33
+ window.addEventListener('mousedown', onDocClick);
34
+ window.addEventListener('keydown', onKey);
35
+ return () => {
36
+ window.removeEventListener('mousedown', onDocClick);
37
+ window.removeEventListener('keydown', onKey);
38
+ };
39
+ }, [open]);
40
+ async function signOut() {
41
+ try {
42
+ await fetch(`${baseUrl}/v1/auth/sign-out`, {
43
+ method: 'POST',
44
+ credentials: 'include',
45
+ });
46
+ }
47
+ finally {
48
+ if (typeof window !== 'undefined')
49
+ window.location.href = afterSignOutUrl;
50
+ }
51
+ }
52
+ if (!auth.isSignedIn)
53
+ return null;
54
+ const email = user?.email ?? '';
55
+ const displayName = user?.firstName
56
+ ? `${user.firstName}${user.lastName ? ' ' + user.lastName : ''}`
57
+ : email.split('@')[0] || 'User';
58
+ const initials = getInitials(displayName, email);
59
+ const avatar = email ? gravatar(email) : null;
60
+ return (_jsxs("div", { ref: rootRef, style: { position: 'relative', display: 'inline-block' }, children: [_jsxs("button", { type: "button", className: "af-user-btn", onClick: () => setOpen((v) => !v), "aria-haspopup": "menu", "aria-expanded": open, children: [_jsxs("span", { className: "af-avatar", "aria-hidden": true, children: [avatar && _jsx("img", { src: avatar, alt: "" }), !avatar && initials] }), _jsx("span", { children: displayName })] }), open && (_jsxs("div", { className: "af-menu", role: "menu", children: [_jsxs("div", { className: "af-menu-head", children: [_jsxs("span", { className: "af-avatar af-avatar-lg", "aria-hidden": true, style: { width: 40, height: 40, fontSize: 12 }, children: [avatar && _jsx("img", { src: avatar, alt: "" }), !avatar && initials] }), _jsxs("div", { style: { minWidth: 0 }, children: [_jsx("div", { className: "af-menu-head-name", children: displayName }), showEmail && email && _jsx("div", { className: "af-menu-head-email", children: email })] })] }), _jsx("a", { href: userProfileUrl, className: "af-menu-item", role: "menuitem", children: "Manage account" }), _jsx("div", { className: "af-menu-sep" }), _jsx("button", { type: "button", className: "af-menu-item af-menu-item-danger", role: "menuitem", onClick: signOut, children: "Sign out" })] }))] }));
61
+ }
62
+ function getInitials(name, email) {
63
+ const src = (name && name.trim()) || email || '?';
64
+ const parts = src.trim().split(/\s+/);
65
+ if (parts.length >= 2)
66
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
67
+ return parts[0].slice(0, 2).toUpperCase();
68
+ }
69
+ function gravatar(email) {
70
+ // md5 of the lowercased, trimmed email — computed at request time in the
71
+ // browser via SubtleCrypto where available, else we fall back to a simple
72
+ // hash (not cryptographically correct for Gravatar, but keeps us from
73
+ // bundling md5). We use Gravatar's "identicon" fallback so everyone gets
74
+ // a unique-looking avatar even without a registered profile.
75
+ const normalized = email.trim().toLowerCase();
76
+ // Tiny djb2 hash → hex; collides like crazy but good enough for fallbacks.
77
+ let h = 5381;
78
+ for (let i = 0; i < normalized.length; i++)
79
+ h = ((h << 5) + h + normalized.charCodeAt(i)) | 0;
80
+ const hex = (h >>> 0).toString(16).padStart(8, '0');
81
+ return `https://www.gravatar.com/avatar/${hex.repeat(4)}?s=80&d=identicon`;
82
+ }
@@ -0,0 +1,19 @@
1
+ type Tab = 'profile' | 'security' | 'sessions';
2
+ export type UserProfileProps = {
3
+ /** Default open tab. */
4
+ initialTab?: Tab;
5
+ /** Called after the user saves profile changes. */
6
+ onUpdate?: (user: {
7
+ firstName: string | null;
8
+ lastName: string | null;
9
+ }) => void;
10
+ };
11
+ /**
12
+ * Self-service user profile card. Tabs: Profile (name/email), Security
13
+ * (password change, future MFA), Sessions (future). Minimal scaffolding —
14
+ * uses the public instance API endpoints available today, with room for
15
+ * future expansion.
16
+ */
17
+ export declare function UserProfile({ initialTab, onUpdate }: UserProfileProps): import("react/jsx-runtime").JSX.Element;
18
+ export {};
19
+ //# sourceMappingURL=UserProfile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserProfile.d.ts","sourceRoot":"","sources":["../../src/ui/UserProfile.tsx"],"names":[],"mappings":"AAQA,KAAK,GAAG,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;AAE/C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wBAAwB;IACxB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;CAClF,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,UAAsB,EAAE,QAAQ,EAAE,EAAE,gBAAgB,2CA2DjF"}
@@ -0,0 +1,199 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { useAuthfyio } from '../provider.js';
5
+ import { useUser } from '../hooks.js';
6
+ import { ensureStylesInjected } from './styles.js';
7
+ /**
8
+ * Self-service user profile card. Tabs: Profile (name/email), Security
9
+ * (password change, future MFA), Sessions (future). Minimal scaffolding —
10
+ * uses the public instance API endpoints available today, with room for
11
+ * future expansion.
12
+ */
13
+ export function UserProfile({ initialTab = 'profile', onUpdate }) {
14
+ useEffect(ensureStylesInjected, []);
15
+ const { client } = useAuthfyio();
16
+ const baseUrl = client.baseUrl;
17
+ const { user, isLoaded } = useUser();
18
+ const [tab, setTab] = useState(initialTab);
19
+ if (!isLoaded) {
20
+ return (_jsx("div", { className: "af-card", style: { maxWidth: 720 }, children: _jsx("div", { className: "af-subtitle", children: "Loading\u2026" }) }));
21
+ }
22
+ if (!user) {
23
+ return (_jsx("div", { className: "af-card", style: { maxWidth: 720 }, children: _jsx("div", { className: "af-error", children: "You are not signed in." }) }));
24
+ }
25
+ return (_jsxs("div", { className: "af-card", style: { maxWidth: 720 }, children: [_jsx("h1", { className: "af-title", children: "Account" }), _jsx("p", { className: "af-subtitle", children: "Manage your profile, credentials, and active sessions." }), _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 === 'profile' ? ' is-active' : ''}`, onClick: () => setTab('profile'), children: "Profile" }), _jsx("button", { role: "tab", className: `af-profile-nav-item${tab === 'security' ? ' is-active' : ''}`, onClick: () => setTab('security'), children: "Security" }), _jsx("button", { role: "tab", className: `af-profile-nav-item${tab === 'sessions' ? ' is-active' : ''}`, onClick: () => setTab('sessions'), children: "Sessions" })] }), _jsxs("div", { className: "af-profile-panel", role: "tabpanel", children: [tab === 'profile' && _jsx(ProfileTab, { baseUrl: baseUrl, user: user, onUpdate: onUpdate }), tab === 'security' && _jsx(SecurityTab, { baseUrl: baseUrl }), tab === 'sessions' && _jsx(SessionsTab, { baseUrl: baseUrl })] })] })] }));
26
+ }
27
+ function ProfileTab({ baseUrl, user, onUpdate, }) {
28
+ const [firstName, setFirstName] = useState(user.firstName ?? '');
29
+ const [lastName, setLastName] = useState(user.lastName ?? '');
30
+ const [saving, setSaving] = useState(false);
31
+ const [saved, setSaved] = useState(false);
32
+ const [error, setError] = useState(null);
33
+ async function save(e) {
34
+ e.preventDefault();
35
+ setSaving(true);
36
+ setError(null);
37
+ setSaved(false);
38
+ try {
39
+ const res = await fetch(`${baseUrl}/v1/sessions/current`, {
40
+ method: 'PATCH',
41
+ credentials: 'include',
42
+ headers: { 'content-type': 'application/json' },
43
+ body: JSON.stringify({
44
+ firstName: firstName || null,
45
+ lastName: lastName || null,
46
+ }),
47
+ });
48
+ if (!res.ok)
49
+ throw new Error('Could not save profile.');
50
+ setSaved(true);
51
+ if (onUpdate)
52
+ onUpdate({ firstName: firstName || null, lastName: lastName || null });
53
+ }
54
+ catch (err) {
55
+ setError(err?.message ?? 'Could not save profile.');
56
+ }
57
+ finally {
58
+ setSaving(false);
59
+ }
60
+ }
61
+ return (_jsxs("form", { onSubmit: save, children: [_jsx("h3", { children: "Profile" }), _jsx("p", { className: "af-sub", children: "Your public identity across this application." }), _jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", htmlFor: "af-first", children: "First name" }), _jsx("input", { id: "af-first", className: "af-input", value: firstName, onChange: (e) => setFirstName(e.target.value), maxLength: 60 })] }), _jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", htmlFor: "af-last", children: "Last name" }), _jsx("input", { id: "af-last", className: "af-input", value: lastName, onChange: (e) => setLastName(e.target.value), maxLength: 60 })] }), _jsxs("div", { className: "af-row", children: [_jsx("span", { className: "af-row-key", children: "Email" }), _jsx("span", { className: "af-row-val", children: user.email ?? '—' })] }), _jsxs("div", { className: "af-row", children: [_jsx("span", { className: "af-row-key", children: "Username" }), _jsx("span", { className: "af-row-val", children: user.username ?? _jsx("span", { style: { color: '#9ca3af' }, children: "Not set" }) })] }), _jsx("div", { className: "af-primary-row", children: _jsx("button", { type: "submit", className: "af-btn af-btn-primary", disabled: saving, style: { width: 'auto' }, children: saving ? 'Saving…' : 'Save changes' }) }), error && _jsx("div", { className: "af-error", children: error }), saved && _jsx("div", { className: "af-notice", children: "Saved." })] }));
62
+ }
63
+ function SecurityTab({ baseUrl }) {
64
+ const [currentPassword, setCurrent] = useState('');
65
+ const [newPassword, setNewPassword] = useState('');
66
+ const [confirm, setConfirm] = useState('');
67
+ const [saving, setSaving] = useState(false);
68
+ const [error, setError] = useState(null);
69
+ const [ok, setOk] = useState(false);
70
+ async function submit(e) {
71
+ e.preventDefault();
72
+ setError(null);
73
+ setOk(false);
74
+ if (newPassword.length < 8)
75
+ return setError('New password must be at least 8 characters.');
76
+ if (newPassword !== confirm)
77
+ return setError('Passwords do not match.');
78
+ setSaving(true);
79
+ try {
80
+ const res = await fetch(`${baseUrl}/v1/auth/me/password`, {
81
+ method: 'POST',
82
+ credentials: 'include',
83
+ headers: { 'content-type': 'application/json' },
84
+ body: JSON.stringify({ currentPassword, newPassword }),
85
+ });
86
+ if (!res.ok)
87
+ throw new Error('Could not update password.');
88
+ setOk(true);
89
+ setCurrent('');
90
+ setNewPassword('');
91
+ setConfirm('');
92
+ }
93
+ catch (err) {
94
+ setError(err?.message ?? 'Could not update password.');
95
+ }
96
+ finally {
97
+ setSaving(false);
98
+ }
99
+ }
100
+ return (_jsxs(_Fragment, { children: [_jsxs("form", { onSubmit: submit, children: [_jsx("h3", { children: "Security" }), _jsx("p", { className: "af-sub", children: "Update your password. Active sessions are not signed out automatically." }), _jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", children: "Current password" }), _jsx("input", { className: "af-input", type: "password", autoComplete: "current-password", required: true, value: currentPassword, onChange: (e) => setCurrent(e.target.value) })] }), _jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", children: "New password" }), _jsx("input", { className: "af-input", type: "password", autoComplete: "new-password", required: true, value: newPassword, onChange: (e) => setNewPassword(e.target.value) })] }), _jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", children: "Confirm new password" }), _jsx("input", { className: "af-input", type: "password", autoComplete: "new-password", required: true, value: confirm, onChange: (e) => setConfirm(e.target.value) })] }), _jsx("div", { className: "af-primary-row", children: _jsx("button", { type: "submit", className: "af-btn af-btn-primary", disabled: saving, style: { width: 'auto' }, children: saving ? 'Updating…' : 'Update password' }) }), error && _jsx("div", { className: "af-error", children: error }), ok && _jsx("div", { className: "af-notice", children: "Password updated." })] }), _jsx(MfaEnrollSection, { baseUrl: baseUrl })] }));
101
+ }
102
+ /**
103
+ * Two-factor (TOTP authenticator) enrollment. Generates a secret, has the user
104
+ * confirm a code from their authenticator app, then reveals one-time backup
105
+ * codes. Mirrors the instance API: /me/totp/generate -> /me/totp/verify ->
106
+ * /me/backup-codes/generate.
107
+ */
108
+ function MfaEnrollSection({ baseUrl }) {
109
+ const [step, setStep] = useState('idle');
110
+ const [secret, setSecret] = useState('');
111
+ const [otpauth, setOtpauth] = useState('');
112
+ const [code, setCode] = useState('');
113
+ const [backupCodes, setBackupCodes] = useState([]);
114
+ const [busy, setBusy] = useState(false);
115
+ const [error, setError] = useState(null);
116
+ async function start() {
117
+ setBusy(true);
118
+ setError(null);
119
+ try {
120
+ const res = await fetch(`${baseUrl}/v1/auth/me/totp/generate`, {
121
+ method: 'POST',
122
+ credentials: 'include',
123
+ });
124
+ if (!res.ok)
125
+ throw new Error('Could not start enrollment.');
126
+ const body = (await res.json());
127
+ setOtpauth(body.otpauth ?? '');
128
+ setSecret(body.secret ?? '');
129
+ setStep('verify');
130
+ }
131
+ catch (err) {
132
+ setError(err?.message ?? 'Could not start enrollment.');
133
+ }
134
+ finally {
135
+ setBusy(false);
136
+ }
137
+ }
138
+ async function confirm(e) {
139
+ e.preventDefault();
140
+ setBusy(true);
141
+ setError(null);
142
+ try {
143
+ const res = await fetch(`${baseUrl}/v1/auth/me/totp/verify`, {
144
+ method: 'POST',
145
+ credentials: 'include',
146
+ headers: { 'content-type': 'application/json' },
147
+ body: JSON.stringify({ code: code.trim() }),
148
+ });
149
+ const body = (await res.json().catch(() => ({})));
150
+ if (!res.ok || !body.ok)
151
+ throw new Error('That code is not valid. Try again.');
152
+ // Issue recovery codes now that the factor is confirmed.
153
+ const bc = await fetch(`${baseUrl}/v1/auth/me/backup-codes/generate`, {
154
+ method: 'POST',
155
+ credentials: 'include',
156
+ });
157
+ const bcBody = (await bc.json().catch(() => ({})));
158
+ setBackupCodes(bcBody.codes ?? []);
159
+ setStep('done');
160
+ }
161
+ catch (err) {
162
+ setError(err?.message ?? 'Verification failed.');
163
+ }
164
+ finally {
165
+ setBusy(false);
166
+ }
167
+ }
168
+ return (_jsxs("div", { style: { marginTop: 28, borderTop: '1px solid var(--af-border, #e5e7eb)', paddingTop: 20 }, children: [_jsx("h3", { children: "Two-step verification" }), _jsx("p", { className: "af-sub", children: "Add an authenticator app (TOTP) for a second factor at sign-in." }), step === 'idle' && (_jsx("div", { className: "af-primary-row", children: _jsx("button", { type: "button", className: "af-btn af-btn-primary", disabled: busy, style: { width: 'auto' }, onClick: start, children: busy ? 'Please wait…' : 'Enable authenticator app' }) })), step === 'verify' && (_jsxs("form", { onSubmit: confirm, children: [_jsx("p", { className: "af-sub", children: "Add this secret key to your authenticator app, then enter the 6-digit code it shows." }), _jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", children: "Secret key" }), _jsx("input", { className: "af-input", readOnly: true, value: secret, onFocus: (e) => e.currentTarget.select() })] }), _jsxs("div", { className: "af-field", children: [_jsx("label", { className: "af-label", children: "Authenticator code" }), _jsx("input", { className: "af-input", inputMode: "numeric", autoComplete: "one-time-code", required: true, value: code, onChange: (e) => setCode(e.target.value), placeholder: "123456" })] }), _jsx("div", { className: "af-primary-row", children: _jsx("button", { type: "submit", className: "af-btn af-btn-primary", disabled: busy, style: { width: 'auto' }, children: busy ? 'Verifying…' : 'Confirm & enable' }) })] })), step === 'done' && (_jsxs("div", { children: [_jsx("div", { className: "af-notice", children: "Two-step verification is on." }), backupCodes.length > 0 && (_jsxs(_Fragment, { children: [_jsx("p", { className: "af-sub", style: { marginTop: 14 }, children: "Save these one-time backup codes somewhere safe \u2014 each works once if you lose your authenticator." }), _jsx("pre", { className: "af-input", style: { whiteSpace: 'pre-wrap', fontFamily: 'monospace' }, children: backupCodes.join('\n') })] }))] })), error && _jsx("div", { className: "af-error", children: error })] }));
169
+ }
170
+ function SessionsTab({ baseUrl }) {
171
+ const [loading, setLoading] = useState(true);
172
+ const [error, setError] = useState(null);
173
+ useEffect(() => {
174
+ let cancelled = false;
175
+ (async () => {
176
+ try {
177
+ // Per-user session listing is on the roadmap; for now we offer a
178
+ // bulk sign-out action that ends every session for the user.
179
+ setLoading(false);
180
+ }
181
+ catch {
182
+ if (!cancelled)
183
+ setError('Could not load sessions.');
184
+ }
185
+ })();
186
+ return () => { cancelled = true; };
187
+ }, []);
188
+ async function endAll() {
189
+ try {
190
+ await fetch(`${baseUrl}/v1/auth/sign-out/all`, { method: 'POST', credentials: 'include' });
191
+ if (typeof window !== 'undefined')
192
+ window.location.href = '/';
193
+ }
194
+ catch (err) {
195
+ setError(err?.message ?? 'Failed to sign out.');
196
+ }
197
+ }
198
+ return (_jsxs("div", { children: [_jsx("h3", { children: "Active sessions" }), _jsx("p", { className: "af-sub", children: "Manage your signed-in devices." }), loading && _jsx("div", { className: "af-subtitle", children: "Loading\u2026" }), error && _jsx("div", { className: "af-error", children: error }), _jsxs("div", { className: "af-row", children: [_jsx("span", { className: "af-row-key", children: "This device" }), _jsx("span", { className: "af-pill af-pill-success", children: "Current" })] }), _jsx("div", { className: "af-primary-row", children: _jsx("button", { type: "button", className: "af-btn af-btn-danger", onClick: endAll, style: { width: 'auto' }, children: "Sign out of all devices" }) })] }));
199
+ }