@protobase/ui 0.14.7 → 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,11 +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";
14
16
  import { SignUpPage } from "./sign-up-page.js";
15
17
  import { readStaffLink, withoutStaffLink } from "./staff-link.js";
16
18
  import { StaffSignInPage } from "./staff-sign-in-page.js";
17
19
  import { TwoFactorPage } from "./two-factor-page.js";
20
+ import { useSignInAddress } from "./use-sign-in-address.js";
18
21
  import { readInvitationLink, withoutInvitationLink } from "../organizations/invitation-link.js";
19
22
  import { InvitationPage } from "../organizations/invitation-page.js";
20
23
  import { useCallback, useEffect, useState } from "react";
@@ -35,8 +38,7 @@ const firstPage = () => {
35
38
  const invitation = readInvitationLink(href);
36
39
  if (invitation) return {
37
40
  kind: "invitation",
38
- token: invitation.token,
39
- signingIn: false
41
+ token: invitation.token
40
42
  };
41
43
  const provider = readSignInLink(href);
42
44
  if (provider) return {
@@ -49,33 +51,62 @@ const firstPage = () => {
49
51
  providerError
50
52
  } : { kind: "sign-in" };
51
53
  };
52
- const forgetResetLink = () => window.history.replaceState(window.history.state, "", withoutResetLink(window.location.href));
53
- const forgetStaffLink = () => window.history.replaceState(window.history.state, "", withoutStaffLink(window.location.href));
54
- const forgetSignInLink = () => window.history.replaceState(window.history.state, "", withoutSignInLink(window.location.href));
55
- 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
+ };
56
60
  /**
57
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
58
- * browse, and the right page otherwise: the sign-in steps, or the page an emailed reset link, an invitation link or a
59
- * 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.
60
66
  */
61
67
  const AuthGate = ({ workspace, children }) => {
62
- const { state, recheck, passwordReset, signOut, signInProviders, showSignIn, canSignUp, entry, showSignUp, 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();
63
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";
64
84
  useEffect(() => {
65
- if (state.kind === "signed-in" || state.kind === "two-factor" || state.kind === "setup-required") setPage((current) => {
66
- if (current.kind === "provider-sign-in") forgetSignInLink();
67
- return current.kind === "email-code" || current.kind === "provider-sign-in" || current.kind === "sign-in" && (current.notice || current.error || current.providerError) ? { kind: "sign-in" } : current;
68
- });
69
- }, [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;
70
96
  useEffect(() => {
71
- if (state.kind === "signed-out" && page.kind === "sign-in" && page.providerError) forgetSignInLink();
72
- }, [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
+ ]);
73
104
  const signInFailed = useCallback((error) => setPage({
74
105
  kind: "sign-in",
75
106
  error
76
107
  }), []);
77
108
  const leaveResetPage = (next) => {
78
- forgetResetLink();
109
+ forget(withoutResetLink);
79
110
  setPage(next);
80
111
  };
81
112
  const passwordChanged = async () => {
@@ -87,7 +118,7 @@ const AuthGate = ({ workspace, children }) => {
87
118
  };
88
119
  const backToSignIn = () => {
89
120
  setPage({ kind: "sign-in" });
90
- signOut().then(showSignIn);
121
+ signOut().then(() => open("sign-in", { replace: true }));
91
122
  };
92
123
  switch (state.kind) {
93
124
  case "loading": return /* @__PURE__ */ jsxs("div", {
@@ -106,36 +137,33 @@ const AuthGate = ({ workspace, children }) => {
106
137
  });
107
138
  case "needs-admin": return /* @__PURE__ */ jsx(FirstRunPage, { onCheckAgain: recheck });
108
139
  }
140
+ if (state.kind === "signed-in" && address) return /* @__PURE__ */ jsx(Redirect, { to: next ?? "/" });
109
141
  if (page.kind === "reset-password") return /* @__PURE__ */ jsx(ResetPasswordPage, {
110
142
  token: page.token,
111
143
  onDone: () => void passwordChanged(),
112
- 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,
113
148
  onBack: () => leaveResetPage({ kind: "sign-in" })
114
149
  });
115
150
  if (page.kind === "staff-sign-in") return /* @__PURE__ */ jsx(StaffSignInPage, {
116
151
  link: page.link,
117
152
  onBack: () => {
118
- forgetStaffLink();
153
+ forget(withoutStaffLink);
119
154
  setPage({ kind: "sign-in" });
120
155
  }
121
156
  });
122
157
  const signedOut = state.kind === "signed-out" || state.kind === "visitor";
123
- 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, {
124
159
  token: page.token,
125
160
  onDone: () => {
126
- forgetInvitationLink();
161
+ forget(withoutInvitationLink);
127
162
  setPage({ kind: "sign-in" });
128
163
  },
129
- onSignIn: () => {
130
- setPage({
131
- ...page,
132
- signingIn: true
133
- });
134
- showSignIn();
135
- }
164
+ onSignIn: () => open()
136
165
  });
137
166
  if (state.kind === "signed-in") return children;
138
- if (state.kind === "visitor" && page.kind === "sign-in" && !page.notice && !page.error && !page.providerError) return children;
139
167
  if (state.kind === "two-factor") return /* @__PURE__ */ jsx(TwoFactorPage, {
140
168
  methods: state.methods,
141
169
  onBack: backToSignIn
@@ -146,26 +174,52 @@ const AuthGate = ({ workspace, children }) => {
146
174
  workspace,
147
175
  onFailed: signInFailed
148
176
  });
149
- if (page.kind === "forgot-password") return /* @__PURE__ */ jsx(ForgotPasswordPage, { onBack: () => setPage({ kind: "sign-in" }) });
150
- if (page.kind === "email-code") return /* @__PURE__ */ jsx(EmailCodePage, {
151
- email: page.email,
152
- onBack: () => setPage({ kind: "sign-in" })
153
- });
154
- if (canSignUp && entry === "sign-up" && page.kind === "sign-in") return /* @__PURE__ */ jsx(SignUpPage, { workspace });
155
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);
156
210
  const providerError = signIn.providerError && providerMessage(signIn.providerError, signInProviders.length === 1 ? signInProviders[0].name : "the provider");
157
211
  const error = signIn.error ?? providerError;
158
212
  return /* @__PURE__ */ jsx(SignInPage, {
159
213
  workspace,
160
214
  notice: signIn.notice,
161
215
  error,
162
- onForgotPassword: passwordReset ? () => setPage({ kind: "forgot-password" }) : void 0,
163
- onCodeSent: (email) => setPage({
164
- kind: "email-code",
165
- email
166
- }),
167
- onSignUp: canSignUp ? showSignUp : void 0,
168
- 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
169
223
  }, error);
170
224
  };
171
225
  //#endregion
@@ -15,7 +15,6 @@ const AuthProvider = ({ session: given, children }) => {
15
15
  const [signInMethods, setSignInMethods] = useState(["password"]);
16
16
  const [passwordReset, setPasswordReset] = useState(false);
17
17
  const [canSignUp, setCanSignUp] = useState(false);
18
- const [entry, setEntry] = useState("sign-in");
19
18
  const [passkeyDomains, setPasskeyDomains] = useState([]);
20
19
  const [socialProviders, setSocialProviders] = useState([]);
21
20
  const [platformSignIn, setPlatformSignIn] = useState();
@@ -29,7 +28,6 @@ const AuthProvider = ({ session: given, children }) => {
29
28
  const session = useMemo(() => given ?? createAuthSession({ onSignedOut: () => signedOut.current() }), [given]);
30
29
  const enter = useCallback(async (user) => {
31
30
  const [{ missing }, staff] = await Promise.all([session.account.signInMethods(), session.staff.current()]);
32
- setEntry("sign-in");
33
31
  setState(missing.length > 0 ? {
34
32
  kind: "setup-required",
35
33
  user,
@@ -71,7 +69,6 @@ const AuthProvider = ({ session: given, children }) => {
71
69
  signInMethods,
72
70
  passwordReset,
73
71
  canSignUp,
74
- entry,
75
72
  passkeyDomains,
76
73
  socialProviders,
77
74
  signInProviders: socialProviders.map((id) => ({
@@ -82,16 +79,7 @@ const AuthProvider = ({ session: given, children }) => {
82
79
  ...organizations && { organizations },
83
80
  markSignedOut,
84
81
  recheck,
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
82
  ...anonymous && { browse: () => {
94
- setEntry("sign-in");
95
83
  setState((current) => current.kind === "signed-out" ? { kind: "visitor" } : current);
96
84
  } },
97
85
  signIn: async (email, password) => finish(await session.signIn(email, password)),
@@ -121,7 +109,6 @@ const AuthProvider = ({ session: given, children }) => {
121
109
  signInMethods,
122
110
  passwordReset,
123
111
  canSignUp,
124
- entry,
125
112
  passkeyDomains,
126
113
  socialProviders,
127
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 };
@@ -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,7 +18,8 @@ const Benefits = ({ benefits }) => benefits && benefits.length > 0 ? /* @__PURE_
17
18
  }, benefit))
18
19
  }) : null;
19
20
  const Actions = ({ prompt }) => {
20
- const { showSignIn, canSignUp, showSignUp } = useAuth();
21
+ const { canSignUp } = useAuth();
22
+ const { open } = useSignInAddress();
21
23
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [prompt.signUp ? /* @__PURE__ */ jsx("a", {
22
24
  href: prompt.signUp.href,
23
25
  target: "_blank",
@@ -25,11 +27,11 @@ const Actions = ({ prompt }) => {
25
27
  className: buttonClass(),
26
28
  children: prompt.signUp.label
27
29
  }) : canSignUp && /* @__PURE__ */ jsx(Button, {
28
- onClick: showSignUp,
30
+ onClick: () => open("sign-up"),
29
31
  children: "Create an account"
30
32
  }), /* @__PURE__ */ jsx(Button, {
31
33
  variant: "primary",
32
- onClick: showSignIn,
34
+ onClick: () => open(),
33
35
  children: "Sign in"
34
36
  })] });
35
37
  };
@@ -10,9 +10,8 @@ import { jsx } from "react/jsx-runtime";
10
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
11
  * password. The new account is signed in, and sets up what the sign-in policy requires as after any sign-in.
12
12
  */
13
- const SignUpPage = ({ workspace }) => {
14
- const { signInMethods, sendSignUpCode, signUp, showSignIn, browse } = useAuth();
15
- const [email, setEmail] = useState();
13
+ const SignUpPage = ({ workspace, email, onCodeSent, onBack, onSignIn, onBrowse }) => {
14
+ const { signInMethods, sendSignUpCode, signUp } = useAuth();
16
15
  const [busy, setBusy] = useState(false);
17
16
  const [error, setError] = useState();
18
17
  const [notice, setNotice] = useState();
@@ -32,11 +31,11 @@ const SignUpPage = ({ workspace }) => {
32
31
  workspace,
33
32
  busy,
34
33
  error,
35
- onSignIn: showSignIn,
36
- onBrowse: browse,
34
+ onSignIn,
35
+ onBrowse,
37
36
  onSubmit: (address) => void attempt(async () => {
38
37
  await sendSignUpCode(address);
39
- setEmail(address);
38
+ onCodeSent(address);
40
39
  })
41
40
  });
42
41
  return /* @__PURE__ */ jsx(SignUpDetailsForm, {
@@ -45,10 +44,7 @@ const SignUpPage = ({ workspace }) => {
45
44
  busy,
46
45
  error,
47
46
  notice,
48
- onBack: () => {
49
- setError(void 0);
50
- setEmail(void 0);
51
- },
47
+ onBack,
52
48
  onResend: () => void attempt(async () => {
53
49
  await sendSignUpCode(email);
54
50
  setNotice("A new code is on its way. Only the newest one works.");
@@ -0,0 +1,27 @@
1
+ import { withoutSignInLink } from "./provider-link.js";
2
+ import { useRouter } from "../app/router.js";
3
+ import { readSignInAddress, signInPath } from "./sign-in-address.js";
4
+ import { useCallback, useMemo } from "react";
5
+ //#region src/auth/use-sign-in-address.ts
6
+ /**
7
+ * The sign-in route as logical values, and the ways to move along it: `address` is the step it shows, or `undefined` on
8
+ * any other page; `next` is the page to come back to; `open` shows a step, and `leave` goes back to `next` in place of
9
+ * the route in the tab's history. Components read and write the sign-in route through this hook, never its query.
10
+ */
11
+ const useSignInAddress = () => {
12
+ const { path, params, navigate } = useRouter();
13
+ const address = useMemo(() => readSignInAddress(path, params), [path, params]);
14
+ const here = withoutSignInLink(new URL(params.size > 0 ? `${path}?${params}` : path, "http://local").href);
15
+ const next = address ? address.next : here;
16
+ return {
17
+ address,
18
+ next,
19
+ open: useCallback((step = "sign-in", options) => navigate(signInPath({
20
+ step,
21
+ next
22
+ }), options), [navigate, next]),
23
+ leave: useCallback(() => navigate(next ?? "/", { replace: true }), [navigate, next])
24
+ };
25
+ };
26
+ //#endregion
27
+ export { useSignInAddress };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.14.7";
2
+ var version = "0.15.0";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };