@protobase/ui 0.14.6 → 0.15.0

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/dist/app/app.d.ts CHANGED
@@ -29,9 +29,9 @@ export type AppProps = {
29
29
  pluginLoader?: PluginLoader;
30
30
  };
31
31
  /**
32
- * The admin: sign-in first, or browsing as a visitor when the project has an anonymous role, then a sidebar from
33
- * `/meta`, `/:page` composed pages, `/:resource` lists and `/:resource/:key` records. Fills its parent, in the theme
34
- * this browser chose, light, dark or the system's.
32
+ * The admin: the sign-in route first, `/-/sign-in`, or browsing as a visitor when the project has an anonymous role,
33
+ * then a sidebar from `/meta`, `/:page` composed pages, `/:resource` lists and `/:resource/:key` records. Fills its
34
+ * parent, in the theme this browser chose, light, dark or the system's.
35
35
  */
36
- export declare const App: ({ auth, workspace, ...rest }: AppProps) => import("react").JSX.Element;
36
+ export declare const App: ({ auth, workspace, basePath, initialUrl, ...rest }: AppProps) => import("react").JSX.Element;
37
37
  //#endregion
package/dist/app/app.js CHANGED
@@ -4,6 +4,8 @@ import { AuthProvider, useAuth } from "../auth/auth-provider.js";
4
4
  import { AccountSecurityPage } from "../account/account-security-page.js";
5
5
  import { SignInPolicyPage } from "../account/sign-in-policy-page.js";
6
6
  import { ApiProvider } from "../data/api-provider.js";
7
+ import { Router, useRouter } from "./router.js";
8
+ import { useSignInAddress } from "../auth/use-sign-in-address.js";
7
9
  import { managesOrganization, organizationMenuItems, organizationPageTitles } from "../organizations/organization-paths.js";
8
10
  import { AuthGate } from "../auth/auth-gate.js";
9
11
  import { isAdmin, userToShell } from "../auth/user-to-shell.js";
@@ -19,7 +21,6 @@ import { bottomBarFromMeta } from "./bottom-bar-from-meta.js";
19
21
  import { breadcrumbFor } from "./breadcrumb-for.js";
20
22
  import { sourceStatusOf } from "../data/with-sources.js";
21
23
  import { MetaGate, Notice, useAdminMeta } from "./meta-gate.js";
22
- import { Router, useRouter } from "./router.js";
23
24
  import { useAppAddress } from "./use-app-address.js";
24
25
  import { CustomBoundary } from "./pages/custom-boundary.js";
25
26
  import { CreatePage } from "./create/create-page.js";
@@ -49,7 +50,8 @@ const placeOf = (address) => address?.kind === "page" ? address.page : address?.
49
50
  const recordOf = (address) => address?.kind === "record" ? address : void 0;
50
51
  const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
51
52
  const meta = useAdminMeta();
52
- const { state, session, signOut, showSignIn, stopStaffSession, organizations: organizationSettings } = useAuth();
53
+ const { state, session, signOut, stopStaffSession, organizations: organizationSettings } = useAuth();
54
+ const signIn = useSignInAddress();
53
55
  if (state.kind !== "signed-in" && state.kind !== "visitor") throw new Error("The shell is only rendered for a signed-in user or a visitor");
54
56
  const user = state.kind === "signed-in" ? state.user : void 0;
55
57
  const staff = state.kind === "signed-in" ? state.staff : void 0;
@@ -172,7 +174,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
172
174
  breadcrumb,
173
175
  user: acting && userToShell(acting),
174
176
  onSignOut: user && (() => void signOut()),
175
- onSignIn: user ? void 0 : showSignIn,
177
+ onSignIn: user ? void 0 : () => signIn.open(),
176
178
  workspace: current?.organization?.name ?? workspace,
177
179
  navGroups: groups,
178
180
  bottomBar: bottomBarFromMeta(meta, basePath, place),
@@ -241,7 +243,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
241
243
  ]
242
244
  });
243
245
  };
244
- const Authenticated = ({ baseUrl, client, sources, basePath, initialUrl, workspace, sidebarMode, ui, assistant, runtime, pluginLoader }) => {
246
+ const Authenticated = ({ baseUrl, client, sources, workspace, sidebarMode, ui, assistant, runtime, pluginLoader }) => {
245
247
  const { state, session, markSignedOut } = useAuth();
246
248
  return /* @__PURE__ */ jsx(ApiProvider, {
247
249
  baseUrl,
@@ -249,34 +251,34 @@ const Authenticated = ({ baseUrl, client, sources, basePath, initialUrl, workspa
249
251
  sources,
250
252
  token: state.kind === "visitor" ? void 0 : session.token,
251
253
  onUnauthenticated: markSignedOut,
252
- children: /* @__PURE__ */ jsx(Router, {
253
- basePath,
254
- initialUrl,
255
- children: /* @__PURE__ */ jsx(MetaGate, { children: /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(PluginGate, {
256
- ui,
257
- ...pluginLoader && { loader: pluginLoader },
258
- children: /* @__PURE__ */ jsx(Routes, {
259
- workspace,
260
- sidebarMode,
261
- assistant,
262
- runtime
263
- })
264
- }) }) })
265
- })
254
+ children: /* @__PURE__ */ jsx(MetaGate, { children: /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(PluginGate, {
255
+ ui,
256
+ ...pluginLoader && { loader: pluginLoader },
257
+ children: /* @__PURE__ */ jsx(Routes, {
258
+ workspace,
259
+ sidebarMode,
260
+ assistant,
261
+ runtime
262
+ })
263
+ }) }) })
266
264
  });
267
265
  };
268
266
  /**
269
- * The admin: sign-in first, or browsing as a visitor when the project has an anonymous role, then a sidebar from
270
- * `/meta`, `/:page` composed pages, `/:resource` lists and `/:resource/:key` records. Fills its parent, in the theme
271
- * this browser chose, light, dark or the system's.
267
+ * The admin: the sign-in route first, `/-/sign-in`, or browsing as a visitor when the project has an anonymous role,
268
+ * then a sidebar from `/meta`, `/:page` composed pages, `/:resource` lists and `/:resource/:key` records. Fills its
269
+ * parent, in the theme this browser chose, light, dark or the system's.
272
270
  */
273
- const App = ({ auth, workspace, ...rest }) => /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(AuthProvider, {
271
+ const App = ({ auth, workspace, basePath, initialUrl, ...rest }) => /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(AuthProvider, {
274
272
  session: auth,
275
- children: /* @__PURE__ */ jsx(AuthGate, {
276
- workspace,
277
- children: /* @__PURE__ */ jsx(Authenticated, {
273
+ children: /* @__PURE__ */ jsx(Router, {
274
+ basePath,
275
+ initialUrl,
276
+ children: /* @__PURE__ */ jsx(AuthGate, {
278
277
  workspace,
279
- ...rest
278
+ children: /* @__PURE__ */ jsx(Authenticated, {
279
+ workspace,
280
+ ...rest
281
+ })
280
282
  })
281
283
  })
282
284
  }) });
@@ -3,13 +3,13 @@ import { formatRelativeTime } from "../format/date.js";
3
3
  import { Button } from "../primitives/button.js";
4
4
  import { keys } from "../data/query-keys.js";
5
5
  import { useClient } from "../data/api-provider.js";
6
+ import { useNavigationGuard, useRouter } from "./router.js";
6
7
  import { useRecord } from "../data/use-record.js";
7
8
  import { recordTitle } from "../live/model-helpers.js";
8
9
  import { humanize } from "../live/naming.js";
9
10
  import { useProjectUi } from "./pages/project-ui.js";
10
11
  import { displayValue } from "../live/display-value.js";
11
12
  import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
12
- import { useNavigationGuard, useRouter } from "./router.js";
13
13
  import { useAppAddress } from "./use-app-address.js";
14
14
  import { ErrorBanner } from "./error-banner.js";
15
15
  import { shownWidget } from "./field-types/shown-widget.js";
@@ -1,6 +1,6 @@
1
1
  import { useClient } from "../../data/api-provider.js";
2
- import { useAdminMeta } from "../meta-gate.js";
3
2
  import { useRouter } from "../router.js";
3
+ import { useAdminMeta } from "../meta-gate.js";
4
4
  import { useDebounced } from "../../lib/use-debounced.js";
5
5
  import { searchFields, searchFilter, searchPlaceholder, searchTargets, toHit } from "./search-model.js";
6
6
  import { useMemo, useState } from "react";
@@ -1,7 +1,7 @@
1
1
  import { useIsXs } from "../lib/use-media-query.js";
2
+ import { Link, useRouter } from "./router.js";
2
3
  import { changedList, currentAddress, listDefaults, listedResource } from "./current-address.js";
3
4
  import { useAdminMeta } from "./meta-gate.js";
4
- import { Link, useRouter } from "./router.js";
5
5
  import { useCallback, useMemo } from "react";
6
6
  import { jsx } from "react/jsx-runtime";
7
7
  import { appPath, readAppPath } from "@protobase/schema";
@@ -3,6 +3,7 @@ import { providerMessage } from "./auth-messages.js";
3
3
  import { readProviderError, readSignInLink, withoutSignInLink } from "./provider-link.js";
4
4
  import { useAuth } from "./auth-provider.js";
5
5
  import { Button } from "../primitives/button.js";
6
+ import { useRouter } from "../app/router.js";
6
7
  import { EmailCodePage } from "./email-code-page.js";
7
8
  import { FirstRunPage } from "./first-run-page.js";
8
9
  import { readResetLink, withoutResetLink } from "./reset-link.js";
@@ -10,10 +11,13 @@ import { ForgotPasswordPage } from "./forgot-password-page.js";
10
11
  import { ProviderSignInPage } from "./provider-sign-in-page.js";
11
12
  import { ResetPasswordPage } from "./reset-password-page.js";
12
13
  import { SetupRequiredPage } from "./setup-required-page.js";
14
+ import { signInPath } from "./sign-in-address.js";
13
15
  import { SignInPage } from "./sign-in-page.js";
16
+ import { SignUpPage } from "./sign-up-page.js";
14
17
  import { readStaffLink, withoutStaffLink } from "./staff-link.js";
15
18
  import { StaffSignInPage } from "./staff-sign-in-page.js";
16
19
  import { TwoFactorPage } from "./two-factor-page.js";
20
+ import { useSignInAddress } from "./use-sign-in-address.js";
17
21
  import { readInvitationLink, withoutInvitationLink } from "../organizations/invitation-link.js";
18
22
  import { InvitationPage } from "../organizations/invitation-page.js";
19
23
  import { useCallback, useEffect, useState } from "react";
@@ -34,8 +38,7 @@ const firstPage = () => {
34
38
  const invitation = readInvitationLink(href);
35
39
  if (invitation) return {
36
40
  kind: "invitation",
37
- token: invitation.token,
38
- signingIn: false
41
+ token: invitation.token
39
42
  };
40
43
  const provider = readSignInLink(href);
41
44
  if (provider) return {
@@ -48,33 +51,62 @@ const firstPage = () => {
48
51
  providerError
49
52
  } : { kind: "sign-in" };
50
53
  };
51
- const forgetResetLink = () => window.history.replaceState(window.history.state, "", withoutResetLink(window.location.href));
52
- const forgetStaffLink = () => window.history.replaceState(window.history.state, "", withoutStaffLink(window.location.href));
53
- const forgetSignInLink = () => window.history.replaceState(window.history.state, "", withoutSignInLink(window.location.href));
54
- const forgetInvitationLink = () => window.history.replaceState(window.history.state, "", withoutInvitationLink(window.location.href));
54
+ /** Moves to `to`, below the base path, in place of this page in the tab's history, as soon as it shows. */
55
+ const Redirect = ({ to }) => {
56
+ const { navigate } = useRouter();
57
+ useEffect(() => navigate(to, { replace: true }), [navigate, to]);
58
+ return null;
59
+ };
55
60
  /**
56
61
  * Shows the app when the user is signed in (or the server has no login), or to a visitor when the project lets them
57
- * browse, and the right page otherwise: the sign-in steps, or the page an emailed reset link, an invitation link or a
58
- * staff link opened. An invited person with an account signs in first and comes back to the invitation.
62
+ * browse, and the right page otherwise: the step of the sign-in route the address names, or the page an emailed reset
63
+ * link, an invitation link or a staff link opened. Someone signed out without a visitor's browsing goes to the sign-in
64
+ * route, and signing in comes back to the page the route was opened from. An invited person with an account signs in
65
+ * first and comes back to the invitation.
59
66
  */
60
67
  const AuthGate = ({ workspace, children }) => {
61
- const { state, recheck, passwordReset, signOut, signInProviders, showSignIn, browse } = useAuth();
68
+ const { state, recheck, passwordReset, signOut, signInProviders, canSignUp, browse } = useAuth();
69
+ const { path, params, basePath, navigate } = useRouter();
70
+ const { address, next, open, leave } = useSignInAddress();
62
71
  const [page, setPage] = useState(firstPage);
72
+ const [codeEmail, setCodeEmail] = useState();
73
+ const [signUpEmail, setSignUpEmail] = useState();
74
+ const href = new URL(`${basePath}${path}${params.size > 0 ? `?${params}` : ""}`, window.location.origin).href;
75
+ const forget = useCallback((without) => {
76
+ const below = without(href).slice(basePath.length);
77
+ navigate(below.startsWith("/") ? below : `/${below}`, { replace: true });
78
+ }, [
79
+ href,
80
+ basePath,
81
+ navigate
82
+ ]);
83
+ const entered = state.kind === "signed-in" || state.kind === "two-factor" || state.kind === "setup-required";
63
84
  useEffect(() => {
64
- if (state.kind === "signed-in" || state.kind === "two-factor" || state.kind === "setup-required") setPage((current) => {
65
- if (current.kind === "provider-sign-in") forgetSignInLink();
66
- return current.kind === "email-code" || current.kind === "provider-sign-in" || current.kind === "sign-in" && (current.notice || current.error || current.providerError) ? { kind: "sign-in" } : current;
67
- });
68
- }, [state.kind]);
85
+ if (!entered) return;
86
+ if (page.kind === "provider-sign-in") forget(withoutSignInLink);
87
+ if (page.kind === "provider-sign-in" || page.kind === "sign-in" && (page.notice || page.error || page.providerError)) setPage({ kind: "sign-in" });
88
+ setCodeEmail(void 0);
89
+ setSignUpEmail(void 0);
90
+ }, [
91
+ entered,
92
+ page,
93
+ forget
94
+ ]);
95
+ const providerErrorLeft = address !== void 0 && readProviderError(href) !== void 0;
69
96
  useEffect(() => {
70
- if (state.kind === "signed-out" && page.kind === "sign-in" && page.providerError) forgetSignInLink();
71
- }, [state.kind, page]);
97
+ if (!entered && page.kind === "sign-in" && page.providerError && providerErrorLeft) forget(withoutSignInLink);
98
+ }, [
99
+ entered,
100
+ page,
101
+ providerErrorLeft,
102
+ forget
103
+ ]);
72
104
  const signInFailed = useCallback((error) => setPage({
73
105
  kind: "sign-in",
74
106
  error
75
107
  }), []);
76
108
  const leaveResetPage = (next) => {
77
- forgetResetLink();
109
+ forget(withoutResetLink);
78
110
  setPage(next);
79
111
  };
80
112
  const passwordChanged = async () => {
@@ -86,7 +118,7 @@ const AuthGate = ({ workspace, children }) => {
86
118
  };
87
119
  const backToSignIn = () => {
88
120
  setPage({ kind: "sign-in" });
89
- signOut().then(showSignIn);
121
+ signOut().then(() => open("sign-in", { replace: true }));
90
122
  };
91
123
  switch (state.kind) {
92
124
  case "loading": return /* @__PURE__ */ jsxs("div", {
@@ -105,36 +137,33 @@ const AuthGate = ({ workspace, children }) => {
105
137
  });
106
138
  case "needs-admin": return /* @__PURE__ */ jsx(FirstRunPage, { onCheckAgain: recheck });
107
139
  }
140
+ if (state.kind === "signed-in" && address) return /* @__PURE__ */ jsx(Redirect, { to: next ?? "/" });
108
141
  if (page.kind === "reset-password") return /* @__PURE__ */ jsx(ResetPasswordPage, {
109
142
  token: page.token,
110
143
  onDone: () => void passwordChanged(),
111
- onRequestNewLink: passwordReset ? () => leaveResetPage({ kind: "forgot-password" }) : void 0,
144
+ onRequestNewLink: passwordReset ? () => {
145
+ setPage({ kind: "sign-in" });
146
+ navigate(signInPath({ step: "forgot-password" }), { replace: true });
147
+ } : void 0,
112
148
  onBack: () => leaveResetPage({ kind: "sign-in" })
113
149
  });
114
150
  if (page.kind === "staff-sign-in") return /* @__PURE__ */ jsx(StaffSignInPage, {
115
151
  link: page.link,
116
152
  onBack: () => {
117
- forgetStaffLink();
153
+ forget(withoutStaffLink);
118
154
  setPage({ kind: "sign-in" });
119
155
  }
120
156
  });
121
157
  const signedOut = state.kind === "signed-out" || state.kind === "visitor";
122
- if (page.kind === "invitation" && (state.kind === "signed-in" || signedOut && !page.signingIn)) return /* @__PURE__ */ jsx(InvitationPage, {
158
+ if (page.kind === "invitation" && (state.kind === "signed-in" || signedOut && !address)) return /* @__PURE__ */ jsx(InvitationPage, {
123
159
  token: page.token,
124
160
  onDone: () => {
125
- forgetInvitationLink();
161
+ forget(withoutInvitationLink);
126
162
  setPage({ kind: "sign-in" });
127
163
  },
128
- onSignIn: () => {
129
- setPage({
130
- ...page,
131
- signingIn: true
132
- });
133
- showSignIn();
134
- }
164
+ onSignIn: () => open()
135
165
  });
136
166
  if (state.kind === "signed-in") return children;
137
- if (state.kind === "visitor" && page.kind === "sign-in" && !page.notice && !page.error && !page.providerError) return children;
138
167
  if (state.kind === "two-factor") return /* @__PURE__ */ jsx(TwoFactorPage, {
139
168
  methods: state.methods,
140
169
  onBack: backToSignIn
@@ -145,24 +174,52 @@ const AuthGate = ({ workspace, children }) => {
145
174
  workspace,
146
175
  onFailed: signInFailed
147
176
  });
148
- if (page.kind === "forgot-password") return /* @__PURE__ */ jsx(ForgotPasswordPage, { onBack: () => setPage({ kind: "sign-in" }) });
149
- if (page.kind === "email-code") return /* @__PURE__ */ jsx(EmailCodePage, {
150
- email: page.email,
151
- onBack: () => setPage({ kind: "sign-in" })
152
- });
153
177
  const signIn = page.kind === "sign-in" ? page : { kind: "sign-in" };
178
+ if (!address) {
179
+ const message = signIn.notice || signIn.error || signIn.providerError;
180
+ return state.kind === "visitor" && !message ? children : /* @__PURE__ */ jsx(Redirect, { to: signInPath({ next }) });
181
+ }
182
+ const step = {
183
+ "sign-in": true,
184
+ "forgot-password": passwordReset,
185
+ "email-code": codeEmail !== void 0,
186
+ "sign-up": canSignUp,
187
+ "sign-up-code": canSignUp && signUpEmail !== void 0
188
+ }[address.step] ? address.step : "sign-in";
189
+ const backTo = (target) => () => open(target, { replace: true });
190
+ const onBrowse = browse && (() => {
191
+ browse();
192
+ leave();
193
+ });
194
+ if (step === "forgot-password") return /* @__PURE__ */ jsx(ForgotPasswordPage, { onBack: backTo("sign-in") });
195
+ if (step === "email-code") return /* @__PURE__ */ jsx(EmailCodePage, {
196
+ email: codeEmail,
197
+ onBack: backTo("sign-in")
198
+ });
199
+ if (step === "sign-up" || step === "sign-up-code") return /* @__PURE__ */ jsx(SignUpPage, {
200
+ workspace,
201
+ email: step === "sign-up-code" ? signUpEmail : void 0,
202
+ onCodeSent: (email) => {
203
+ setSignUpEmail(email);
204
+ open("sign-up-code");
205
+ },
206
+ onBack: backTo("sign-up"),
207
+ onSignIn: () => open("sign-in"),
208
+ onBrowse
209
+ }, step);
154
210
  const providerError = signIn.providerError && providerMessage(signIn.providerError, signInProviders.length === 1 ? signInProviders[0].name : "the provider");
155
211
  const error = signIn.error ?? providerError;
156
212
  return /* @__PURE__ */ jsx(SignInPage, {
157
213
  workspace,
158
214
  notice: signIn.notice,
159
215
  error,
160
- onForgotPassword: passwordReset ? () => setPage({ kind: "forgot-password" }) : void 0,
161
- onCodeSent: (email) => setPage({
162
- kind: "email-code",
163
- email
164
- }),
165
- onBrowse: browse
216
+ onForgotPassword: passwordReset ? () => open("forgot-password") : void 0,
217
+ onCodeSent: (email) => {
218
+ setCodeEmail(email);
219
+ open("email-code");
220
+ },
221
+ onSignUp: canSignUp ? () => open("sign-up") : void 0,
222
+ onBrowse
166
223
  }, error);
167
224
  };
168
225
  //#endregion
@@ -14,6 +14,7 @@ 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);
17
18
  const [passkeyDomains, setPasskeyDomains] = useState([]);
18
19
  const [socialProviders, setSocialProviders] = useState([]);
19
20
  const [platformSignIn, setPlatformSignIn] = useState();
@@ -46,6 +47,7 @@ const AuthProvider = ({ session: given, children }) => {
46
47
  session.status().then(async (status) => {
47
48
  setSignInMethods(status.signInMethods);
48
49
  setPasswordReset(status.passwordReset);
50
+ setCanSignUp(Boolean(status.signUp));
49
51
  setPasskeyDomains(status.passkeyDomains ?? []);
50
52
  setSocialProviders(status.socialProviders);
51
53
  setPlatformSignIn(status.platformSignIn);
@@ -66,6 +68,7 @@ const AuthProvider = ({ session: given, children }) => {
66
68
  session,
67
69
  signInMethods,
68
70
  passwordReset,
71
+ canSignUp,
69
72
  passkeyDomains,
70
73
  socialProviders,
71
74
  signInProviders: socialProviders.map((id) => ({
@@ -76,11 +79,14 @@ const AuthProvider = ({ session: given, children }) => {
76
79
  ...organizations && { organizations },
77
80
  markSignedOut,
78
81
  recheck,
79
- showSignIn: () => setState((current) => current.kind === "visitor" ? { kind: "signed-out" } : current),
80
- ...anonymous && { browse: () => setState((current) => current.kind === "signed-out" ? { kind: "visitor" } : current) },
82
+ ...anonymous && { browse: () => {
83
+ setState((current) => current.kind === "signed-out" ? { kind: "visitor" } : current);
84
+ } },
81
85
  signIn: async (email, password) => finish(await session.signIn(email, password)),
82
86
  sendSignInCode: (email) => session.sendSignInCode(email),
83
87
  signInWithCode: async (email, code) => finish(await session.signInWithCode(email, code)),
88
+ sendSignUpCode: (email) => session.sendSignUpCode(email),
89
+ signUp: async (input) => enter(await session.signUp(input)),
84
90
  signInWithPasskey: async (domain) => enter(await session.signInWithPasskey(domain)),
85
91
  verifyTwoFactor: async (input) => enter(await session.verifyTwoFactor(input)),
86
92
  signInSocial: async (provider) => {
@@ -102,6 +108,7 @@ const AuthProvider = ({ session: given, children }) => {
102
108
  session,
103
109
  signInMethods,
104
110
  passwordReset,
111
+ canSignUp,
105
112
  passkeyDomains,
106
113
  socialProviders,
107
114
  platformSignIn,
@@ -0,0 +1,47 @@
1
+ //#region src/auth/sign-in-address.ts
2
+ /** The path of the sign-in route, below the app's base path. Its steps and the page to come back to are in its query. */
3
+ const signInRoute = "/-/sign-in";
4
+ /** The names of the sign-in route's query parameters: the step it shows, and the page to come back to after it. */
5
+ const signInParams = {
6
+ step: "step",
7
+ next: "next"
8
+ };
9
+ /**
10
+ * A step of signing in or making an account, each with an address of its own: Back from a step returns to the one
11
+ * before. `email-code` and `sign-up-code` enter the code emailed to an address the step before asked for.
12
+ */
13
+ const signInSteps = [
14
+ "sign-in",
15
+ "forgot-password",
16
+ "email-code",
17
+ "sign-up",
18
+ "sign-up-code"
19
+ ];
20
+ const isStep = (value) => signInSteps.includes(value);
21
+ const localPage = (next) => {
22
+ if (!next?.startsWith("/")) return void 0;
23
+ const url = new URL(next, "http://local");
24
+ if (url.origin !== "http://local" || url.pathname === "/-/sign-in") return void 0;
25
+ return `${url.pathname}${url.search}`;
26
+ };
27
+ /** The path below the base path of a step of the sign-in route, coming back to `next` after it, the start by default. */
28
+ const signInPath = ({ step = "sign-in", next } = {}) => {
29
+ const params = new URLSearchParams();
30
+ if (step !== "sign-in") params.set(signInParams.step, step);
31
+ const page = localPage(next);
32
+ if (page && page !== "/") params.set(signInParams.next, page);
33
+ const query = params.toString();
34
+ return query ? `${signInRoute}?${query}` : signInRoute;
35
+ };
36
+ /** The sign-in route's step and the page to come back to, for the path below the base path; `undefined` elsewhere. */
37
+ const readSignInAddress = (path, params) => {
38
+ if (path !== "/-/sign-in") return void 0;
39
+ const step = params.get(signInParams.step);
40
+ const next = localPage(params.get(signInParams.next));
41
+ return {
42
+ step: isStep(step) ? step : "sign-in",
43
+ ...next && { next }
44
+ };
45
+ };
46
+ //#endregion
47
+ export { readSignInAddress, signInParams, signInPath, signInRoute, signInSteps };
@@ -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;
@@ -1,5 +1,6 @@
1
1
  import { useAuth } from "./auth-provider.js";
2
2
  import { Button, buttonClass } from "../primitives/button.js";
3
+ import { useSignInAddress } from "./use-sign-in-address.js";
3
4
  import { namedIcon } from "../app/named-icon.js";
4
5
  import { ResponsiveDialog } from "../primitives/responsive-dialog.js";
5
6
  import { useId } from "react";
@@ -17,22 +18,27 @@ const Benefits = ({ benefits }) => benefits && benefits.length > 0 ? /* @__PURE_
17
18
  }, benefit))
18
19
  }) : null;
19
20
  const Actions = ({ prompt }) => {
20
- const { showSignIn } = useAuth();
21
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [prompt.signUp && /* @__PURE__ */ jsx("a", {
21
+ const { canSignUp } = useAuth();
22
+ const { open } = useSignInAddress();
23
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [prompt.signUp ? /* @__PURE__ */ jsx("a", {
22
24
  href: prompt.signUp.href,
23
25
  target: "_blank",
24
26
  rel: "noreferrer",
25
27
  className: buttonClass(),
26
28
  children: prompt.signUp.label
29
+ }) : canSignUp && /* @__PURE__ */ jsx(Button, {
30
+ onClick: () => open("sign-up"),
31
+ children: "Create an account"
27
32
  }), /* @__PURE__ */ jsx(Button, {
28
33
  variant: "primary",
29
- onClick: showSignIn,
34
+ onClick: () => open(),
30
35
  children: "Sign in"
31
36
  })] });
32
37
  };
33
38
  /**
34
39
  * 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
40
+ * signing in gives and its benefits, with "Sign in" and a way to make an account: the page the prompt names, or the
41
+ * app's own sign-up when the server has it. As a page
36
42
  * 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
43
  */
38
44
  const SignInPrompt = (props) => {