@protobase/ui 0.14.6 → 0.14.7

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.
@@ -11,6 +11,7 @@ import { ProviderSignInPage } from "./provider-sign-in-page.js";
11
11
  import { ResetPasswordPage } from "./reset-password-page.js";
12
12
  import { SetupRequiredPage } from "./setup-required-page.js";
13
13
  import { SignInPage } from "./sign-in-page.js";
14
+ import { SignUpPage } from "./sign-up-page.js";
14
15
  import { readStaffLink, withoutStaffLink } from "./staff-link.js";
15
16
  import { StaffSignInPage } from "./staff-sign-in-page.js";
16
17
  import { TwoFactorPage } from "./two-factor-page.js";
@@ -58,7 +59,7 @@ const forgetInvitationLink = () => window.history.replaceState(window.history.st
58
59
  * staff link opened. An invited person with an account signs in first and comes back to the invitation.
59
60
  */
60
61
  const AuthGate = ({ workspace, children }) => {
61
- const { state, recheck, passwordReset, signOut, signInProviders, showSignIn, browse } = useAuth();
62
+ const { state, recheck, passwordReset, signOut, signInProviders, showSignIn, canSignUp, entry, showSignUp, browse } = useAuth();
62
63
  const [page, setPage] = useState(firstPage);
63
64
  useEffect(() => {
64
65
  if (state.kind === "signed-in" || state.kind === "two-factor" || state.kind === "setup-required") setPage((current) => {
@@ -150,6 +151,7 @@ const AuthGate = ({ workspace, children }) => {
150
151
  email: page.email,
151
152
  onBack: () => setPage({ kind: "sign-in" })
152
153
  });
154
+ if (canSignUp && entry === "sign-up" && page.kind === "sign-in") return /* @__PURE__ */ jsx(SignUpPage, { workspace });
153
155
  const signIn = page.kind === "sign-in" ? page : { kind: "sign-in" };
154
156
  const providerError = signIn.providerError && providerMessage(signIn.providerError, signInProviders.length === 1 ? signInProviders[0].name : "the provider");
155
157
  const error = signIn.error ?? providerError;
@@ -162,6 +164,7 @@ const AuthGate = ({ workspace, children }) => {
162
164
  kind: "email-code",
163
165
  email
164
166
  }),
167
+ onSignUp: canSignUp ? showSignUp : void 0,
165
168
  onBrowse: browse
166
169
  }, error);
167
170
  };
@@ -14,6 +14,8 @@ const AuthProvider = ({ session: given, children }) => {
14
14
  const [state, setState] = useState({ kind: "loading" });
15
15
  const [signInMethods, setSignInMethods] = useState(["password"]);
16
16
  const [passwordReset, setPasswordReset] = useState(false);
17
+ const [canSignUp, setCanSignUp] = useState(false);
18
+ const [entry, setEntry] = useState("sign-in");
17
19
  const [passkeyDomains, setPasskeyDomains] = useState([]);
18
20
  const [socialProviders, setSocialProviders] = useState([]);
19
21
  const [platformSignIn, setPlatformSignIn] = useState();
@@ -27,6 +29,7 @@ const AuthProvider = ({ session: given, children }) => {
27
29
  const session = useMemo(() => given ?? createAuthSession({ onSignedOut: () => signedOut.current() }), [given]);
28
30
  const enter = useCallback(async (user) => {
29
31
  const [{ missing }, staff] = await Promise.all([session.account.signInMethods(), session.staff.current()]);
32
+ setEntry("sign-in");
30
33
  setState(missing.length > 0 ? {
31
34
  kind: "setup-required",
32
35
  user,
@@ -46,6 +49,7 @@ const AuthProvider = ({ session: given, children }) => {
46
49
  session.status().then(async (status) => {
47
50
  setSignInMethods(status.signInMethods);
48
51
  setPasswordReset(status.passwordReset);
52
+ setCanSignUp(Boolean(status.signUp));
49
53
  setPasskeyDomains(status.passkeyDomains ?? []);
50
54
  setSocialProviders(status.socialProviders);
51
55
  setPlatformSignIn(status.platformSignIn);
@@ -66,6 +70,8 @@ const AuthProvider = ({ session: given, children }) => {
66
70
  session,
67
71
  signInMethods,
68
72
  passwordReset,
73
+ canSignUp,
74
+ entry,
69
75
  passkeyDomains,
70
76
  socialProviders,
71
77
  signInProviders: socialProviders.map((id) => ({
@@ -76,11 +82,23 @@ const AuthProvider = ({ session: given, children }) => {
76
82
  ...organizations && { organizations },
77
83
  markSignedOut,
78
84
  recheck,
79
- showSignIn: () => setState((current) => current.kind === "visitor" ? { kind: "signed-out" } : current),
80
- ...anonymous && { browse: () => setState((current) => current.kind === "signed-out" ? { kind: "visitor" } : current) },
85
+ showSignIn: () => {
86
+ setEntry("sign-in");
87
+ setState((current) => current.kind === "visitor" ? { kind: "signed-out" } : current);
88
+ },
89
+ showSignUp: () => {
90
+ setEntry("sign-up");
91
+ setState((current) => current.kind === "visitor" ? { kind: "signed-out" } : current);
92
+ },
93
+ ...anonymous && { browse: () => {
94
+ setEntry("sign-in");
95
+ setState((current) => current.kind === "signed-out" ? { kind: "visitor" } : current);
96
+ } },
81
97
  signIn: async (email, password) => finish(await session.signIn(email, password)),
82
98
  sendSignInCode: (email) => session.sendSignInCode(email),
83
99
  signInWithCode: async (email, code) => finish(await session.signInWithCode(email, code)),
100
+ sendSignUpCode: (email) => session.sendSignUpCode(email),
101
+ signUp: async (input) => enter(await session.signUp(input)),
84
102
  signInWithPasskey: async (domain) => enter(await session.signInWithPasskey(domain)),
85
103
  verifyTwoFactor: async (input) => enter(await session.verifyTwoFactor(input)),
86
104
  signInSocial: async (provider) => {
@@ -102,6 +120,8 @@ const AuthProvider = ({ session: given, children }) => {
102
120
  session,
103
121
  signInMethods,
104
122
  passwordReset,
123
+ canSignUp,
124
+ entry,
105
125
  passkeyDomains,
106
126
  socialProviders,
107
127
  platformSignIn,
@@ -14,7 +14,7 @@ const Divider = () => /* @__PURE__ */ jsxs("div", {
14
14
  /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" })
15
15
  ]
16
16
  });
17
- const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["password"], onSendCode, onPasskey, passkeyDomains = [], onForgotPassword, providers = [], onContinueWith, onBrowse }) => {
17
+ const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["password"], onSendCode, onPasskey, passkeyDomains = [], onForgotPassword, providers = [], onContinueWith, onSignUp, onBrowse }) => {
18
18
  const [email, setEmail] = useState("");
19
19
  const [password, setPassword] = useState("");
20
20
  const emailInput = useRef(null);
@@ -133,6 +133,19 @@ const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["pass
133
133
  })
134
134
  ]
135
135
  }),
136
+ onSignUp && /* @__PURE__ */ jsxs("p", {
137
+ className: "mt-4 text-center text-[13px] text-muted-foreground",
138
+ children: [
139
+ "New here?",
140
+ " ",
141
+ /* @__PURE__ */ jsx("button", {
142
+ type: "button",
143
+ onClick: onSignUp,
144
+ className: "font-medium text-primary-text hover:underline",
145
+ children: "Create an account"
146
+ })
147
+ ]
148
+ }),
136
149
  onBrowse && /* @__PURE__ */ jsx("button", {
137
150
  type: "button",
138
151
  onClick: onBrowse,
@@ -5,7 +5,7 @@ import { useState } from "react";
5
5
  import { AuthError } from "@protobase/client";
6
6
  import { jsx } from "react/jsx-runtime";
7
7
  //#region src/auth/sign-in-page.tsx
8
- const SignInPage = ({ workspace, notice, onForgotPassword, onBrowse, onCodeSent, error: initialError }) => {
8
+ const SignInPage = ({ workspace, notice, onForgotPassword, onSignUp, onBrowse, onCodeSent, error: initialError }) => {
9
9
  const { signIn, signInSocial, signInProviders, signInMethods, passkeyDomains, sendSignInCode, signInWithPasskey } = useAuth();
10
10
  const [busy, setBusy] = useState(false);
11
11
  const [error, setError] = useState(initialError);
@@ -27,6 +27,7 @@ const SignInPage = ({ workspace, notice, onForgotPassword, onBrowse, onCodeSent,
27
27
  notice,
28
28
  methods: signInMethods,
29
29
  onForgotPassword,
30
+ onSignUp,
30
31
  onBrowse,
31
32
  providers: signInProviders,
32
33
  onContinueWith: (provider) => void attempt(() => signInSocial(provider), signInMessage),
@@ -15,7 +15,8 @@ export type SignInPromptProps = {
15
15
  });
16
16
  /**
17
17
  * What a visitor who is not signed in is told about something they could use once signed in: the prompt's title, what
18
- * signing in gives and its benefits, with "Sign in" and, when the prompt names one, a page to make an account. As a page
18
+ * signing in gives and its benefits, with "Sign in" and a way to make an account: the page the prompt names, or the
19
+ * app's own sign-up when the server has it. As a page
19
20
  * it stands in for a list the visitor cannot see; as a dialog it answers a press on a button that needs an account.
20
21
  */
21
22
  export declare const SignInPrompt: (props: SignInPromptProps) => import("react").JSX.Element;
@@ -17,13 +17,16 @@ const Benefits = ({ benefits }) => benefits && benefits.length > 0 ? /* @__PURE_
17
17
  }, benefit))
18
18
  }) : null;
19
19
  const Actions = ({ prompt }) => {
20
- const { showSignIn } = useAuth();
21
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [prompt.signUp && /* @__PURE__ */ jsx("a", {
20
+ const { showSignIn, canSignUp, showSignUp } = useAuth();
21
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [prompt.signUp ? /* @__PURE__ */ jsx("a", {
22
22
  href: prompt.signUp.href,
23
23
  target: "_blank",
24
24
  rel: "noreferrer",
25
25
  className: buttonClass(),
26
26
  children: prompt.signUp.label
27
+ }) : canSignUp && /* @__PURE__ */ jsx(Button, {
28
+ onClick: showSignUp,
29
+ children: "Create an account"
27
30
  }), /* @__PURE__ */ jsx(Button, {
28
31
  variant: "primary",
29
32
  onClick: showSignIn,
@@ -32,7 +35,8 @@ const Actions = ({ prompt }) => {
32
35
  };
33
36
  /**
34
37
  * What a visitor who is not signed in is told about something they could use once signed in: the prompt's title, what
35
- * signing in gives and its benefits, with "Sign in" and, when the prompt names one, a page to make an account. As a page
38
+ * signing in gives and its benefits, with "Sign in" and a way to make an account: the page the prompt names, or the
39
+ * app's own sign-up when the server has it. As a page
36
40
  * it stands in for a list the visitor cannot see; as a dialog it answers a press on a button that needs an account.
37
41
  */
38
42
  const SignInPrompt = (props) => {
@@ -0,0 +1,130 @@
1
+ import { Input } from "../primitives/input.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { CodeInput } from "./code-input.js";
4
+ import { AuthLayout } from "./auth-layout.js";
5
+ import { useId, useState } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/auth/sign-up-details-form.tsx
8
+ /** The second step of making an account: the emailed code, the person's name and how they will sign in. */
9
+ const SignUpDetailsForm = ({ email, onSubmit, onResend, onBack, methods, busy, error, notice }) => {
10
+ const [code, setCode] = useState("");
11
+ const [name, setName] = useState("");
12
+ const [password, setPassword] = useState("");
13
+ const passwordHint = useId();
14
+ const withPassword = methods.includes("password");
15
+ const passwordOptional = methods.includes("emailCode");
16
+ return /* @__PURE__ */ jsxs(AuthLayout, {
17
+ title: "Check your email",
18
+ description: "Enter the code we sent to create your account.",
19
+ children: [
20
+ /* @__PURE__ */ jsxs("p", {
21
+ className: "mb-4 text-[13px] text-muted-foreground",
22
+ children: [
23
+ "A 6-digit code is on its way to ",
24
+ /* @__PURE__ */ jsx("span", {
25
+ className: "font-medium text-foreground",
26
+ children: email
27
+ }),
28
+ ". It works for 5 minutes, once. If the address has an account already, the email says so instead."
29
+ ]
30
+ }),
31
+ notice && /* @__PURE__ */ jsx("p", {
32
+ role: "status",
33
+ className: "mb-4 rounded-md bg-success-soft px-3 py-2 text-xs font-medium text-success-text",
34
+ children: notice
35
+ }),
36
+ /* @__PURE__ */ jsxs("form", {
37
+ className: "flex flex-col gap-4",
38
+ onSubmit: (event) => {
39
+ event.preventDefault();
40
+ onSubmit({
41
+ code: code.trim(),
42
+ name: name.trim(),
43
+ ...withPassword && password && { password }
44
+ });
45
+ },
46
+ children: [
47
+ /* @__PURE__ */ jsxs("label", {
48
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
49
+ children: ["Code", /* @__PURE__ */ jsx(CodeInput, {
50
+ name: "code",
51
+ required: true,
52
+ autoFocus: true,
53
+ value: code,
54
+ invalid: Boolean(error),
55
+ onChange: (event) => setCode(event.target.value)
56
+ })]
57
+ }),
58
+ /* @__PURE__ */ jsxs("label", {
59
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
60
+ children: ["Your name", /* @__PURE__ */ jsx(Input, {
61
+ name: "name",
62
+ autoComplete: "name",
63
+ required: true,
64
+ maxLength: 200,
65
+ value: name,
66
+ onChange: (event) => setName(event.target.value),
67
+ className: "min-h-6"
68
+ })]
69
+ }),
70
+ withPassword && /* @__PURE__ */ jsxs("div", {
71
+ className: "flex flex-col gap-1.5",
72
+ children: [/* @__PURE__ */ jsxs("label", {
73
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
74
+ children: [passwordOptional ? "Password (optional)" : "Password", /* @__PURE__ */ jsx(Input, {
75
+ type: "password",
76
+ name: "password",
77
+ autoComplete: "new-password",
78
+ "aria-describedby": passwordHint,
79
+ required: !passwordOptional,
80
+ minLength: 12,
81
+ maxLength: 128,
82
+ value: password,
83
+ onChange: (event) => setPassword(event.target.value),
84
+ className: "min-h-6"
85
+ })]
86
+ }), /* @__PURE__ */ jsxs("p", {
87
+ id: passwordHint,
88
+ className: "text-xs text-muted-foreground",
89
+ children: [
90
+ "At least ",
91
+ 12,
92
+ " characters.",
93
+ passwordOptional && " Without one, you sign in with a code we email you."
94
+ ]
95
+ })]
96
+ }),
97
+ error && /* @__PURE__ */ jsx("p", {
98
+ role: "alert",
99
+ className: "text-xs font-medium text-danger-text",
100
+ children: error
101
+ }),
102
+ /* @__PURE__ */ jsx(Button, {
103
+ variant: "primary",
104
+ type: "submit",
105
+ loading: busy,
106
+ className: "min-h-10",
107
+ children: "Create account"
108
+ }),
109
+ /* @__PURE__ */ jsxs("div", {
110
+ className: "flex justify-between gap-2",
111
+ children: [/* @__PURE__ */ jsx(Button, {
112
+ variant: "ghost",
113
+ onClick: onBack,
114
+ className: "min-h-10",
115
+ children: "Use another address"
116
+ }), /* @__PURE__ */ jsx(Button, {
117
+ variant: "ghost",
118
+ onClick: onResend,
119
+ disabled: busy,
120
+ className: "min-h-10",
121
+ children: "Send a new code"
122
+ })]
123
+ })
124
+ ]
125
+ })
126
+ ]
127
+ });
128
+ };
129
+ //#endregion
130
+ export { SignUpDetailsForm };
@@ -0,0 +1,74 @@
1
+ import { Input } from "../primitives/input.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { AuthLayout } from "./auth-layout.js";
4
+ import { useState } from "react";
5
+ import { Mail } from "lucide-react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/auth/sign-up-email-form.tsx
8
+ /** The first step of making an account: the address, which gets a code to prove it is theirs. */
9
+ const SignUpEmailForm = ({ onSubmit, onSignIn, onBrowse, busy, error, workspace }) => {
10
+ const [email, setEmail] = useState("");
11
+ return /* @__PURE__ */ jsxs(AuthLayout, {
12
+ title: "Create an account",
13
+ description: workspace,
14
+ children: [/* @__PURE__ */ jsxs("form", {
15
+ className: "flex flex-col gap-4",
16
+ onSubmit: (event) => {
17
+ event.preventDefault();
18
+ onSubmit(email.trim());
19
+ },
20
+ children: [
21
+ /* @__PURE__ */ jsxs("label", {
22
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
23
+ children: ["Email", /* @__PURE__ */ jsx(Input, {
24
+ type: "email",
25
+ name: "email",
26
+ autoComplete: "email",
27
+ required: true,
28
+ autoFocus: true,
29
+ value: email,
30
+ invalid: Boolean(error),
31
+ onChange: (event) => setEmail(event.target.value),
32
+ className: "min-h-6"
33
+ })]
34
+ }),
35
+ /* @__PURE__ */ jsx("p", {
36
+ className: "text-[13px] text-muted-foreground",
37
+ children: "We email you a code to make sure the address is yours."
38
+ }),
39
+ error && /* @__PURE__ */ jsx("p", {
40
+ role: "alert",
41
+ className: "text-xs font-medium text-danger-text",
42
+ children: error
43
+ }),
44
+ /* @__PURE__ */ jsxs(Button, {
45
+ variant: "primary",
46
+ type: "submit",
47
+ loading: busy,
48
+ className: "min-h-10",
49
+ children: [/* @__PURE__ */ jsx(Mail, { className: "size-4" }), "Email me a code"]
50
+ }),
51
+ /* @__PURE__ */ jsxs("p", {
52
+ className: "text-center text-[13px] text-muted-foreground",
53
+ children: [
54
+ "Have an account?",
55
+ " ",
56
+ /* @__PURE__ */ jsx("button", {
57
+ type: "button",
58
+ onClick: onSignIn,
59
+ className: "font-medium text-primary-text hover:underline",
60
+ children: "Sign in"
61
+ })
62
+ ]
63
+ })
64
+ ]
65
+ }), onBrowse && /* @__PURE__ */ jsx("button", {
66
+ type: "button",
67
+ onClick: onBrowse,
68
+ className: "mt-4 w-full text-center text-[13px] font-medium text-muted-foreground hover:text-foreground hover:underline",
69
+ children: "Continue without an account"
70
+ })]
71
+ });
72
+ };
73
+ //#endregion
74
+ export { SignUpEmailForm };
@@ -0,0 +1,63 @@
1
+ import { authMessage } from "./auth-messages.js";
2
+ import { useAuth } from "./auth-provider.js";
3
+ import { SignUpDetailsForm } from "./sign-up-details-form.js";
4
+ import { SignUpEmailForm } from "./sign-up-email-form.js";
5
+ import { useState } from "react";
6
+ import { AuthError } from "@protobase/client";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/auth/sign-up-page.tsx
9
+ /**
10
+ * Makes an account on a server with open sign-up: the address, then the code emailed to it with the person's name and
11
+ * password. The new account is signed in, and sets up what the sign-in policy requires as after any sign-in.
12
+ */
13
+ const SignUpPage = ({ workspace }) => {
14
+ const { signInMethods, sendSignUpCode, signUp, showSignIn, browse } = useAuth();
15
+ const [email, setEmail] = useState();
16
+ const [busy, setBusy] = useState(false);
17
+ const [error, setError] = useState();
18
+ const [notice, setNotice] = useState();
19
+ const attempt = async (step) => {
20
+ setBusy(true);
21
+ setError(void 0);
22
+ setNotice(void 0);
23
+ try {
24
+ await step();
25
+ } catch (failure) {
26
+ if (!(failure instanceof AuthError)) throw failure;
27
+ setError(authMessage(failure));
28
+ }
29
+ setBusy(false);
30
+ };
31
+ if (email === void 0) return /* @__PURE__ */ jsx(SignUpEmailForm, {
32
+ workspace,
33
+ busy,
34
+ error,
35
+ onSignIn: showSignIn,
36
+ onBrowse: browse,
37
+ onSubmit: (address) => void attempt(async () => {
38
+ await sendSignUpCode(address);
39
+ setEmail(address);
40
+ })
41
+ });
42
+ return /* @__PURE__ */ jsx(SignUpDetailsForm, {
43
+ email,
44
+ methods: signInMethods,
45
+ busy,
46
+ error,
47
+ notice,
48
+ onBack: () => {
49
+ setError(void 0);
50
+ setEmail(void 0);
51
+ },
52
+ onResend: () => void attempt(async () => {
53
+ await sendSignUpCode(email);
54
+ setNotice("A new code is on its way. Only the newest one works.");
55
+ }),
56
+ onSubmit: (details) => void attempt(() => signUp({
57
+ email,
58
+ ...details
59
+ }))
60
+ });
61
+ };
62
+ //#endregion
63
+ export { SignUpPage };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.14.6";
2
+ var version = "0.14.7";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };