@protobase/ui 0.46.8 → 0.46.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.
@@ -2,9 +2,9 @@ import { text } from "./messages.js";
2
2
  import { Button } from "../primitives/button.js";
3
3
  import { Dialog } from "../primitives/dialog.js";
4
4
  import { Input } from "../primitives/input.js";
5
+ import { OtpInput } from "../primitives/otp-input.js";
5
6
  import { SectionError } from "./account-section.js";
6
7
  import { authMessage } from "../auth/auth-messages.js";
7
- import { OtpInput } from "../primitives/otp-input.js";
8
8
  import { useState } from "react";
9
9
  import { AuthError } from "@protobase/client";
10
10
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -2,8 +2,8 @@ import { text } from "./messages.js";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { Button } from "../primitives/button.js";
4
4
  import { Input } from "../primitives/input.js";
5
- import { authMessage } from "../auth/auth-messages.js";
6
5
  import { OtpInput } from "../primitives/otp-input.js";
6
+ import { authMessage } from "../auth/auth-messages.js";
7
7
  import { BackupCodes } from "./backup-codes.js";
8
8
  import { TotpQr, totpKey } from "./totp-qr.js";
9
9
  import { useState } from "react";
@@ -0,0 +1,22 @@
1
+ import { defineMessages } from "../locale/messages.js";
2
+ //#region src/app/confirm-sign-in-messages.ts
3
+ const text = defineMessages({
4
+ en: {
5
+ confirmTitle: "Confirm it's you",
6
+ confirmWhy: (minutes) => `Showing this needs a passkey or your authenticator app in the last ${minutes} minutes.`,
7
+ usePasskey: "Use a passkey",
8
+ authenticatorCode: "Code from your authenticator app",
9
+ confirm: "Confirm",
10
+ nothingToConfirmWith: "Add a passkey or an authenticator app on your account page first."
11
+ },
12
+ nl: {
13
+ confirmTitle: "Bevestig dat jij het bent",
14
+ confirmWhy: (minutes) => `Dit tonen vraagt in de laatste ${minutes} minuten een passkey of je authenticator-app.`,
15
+ usePasskey: "Passkey gebruiken",
16
+ authenticatorCode: "Code uit je authenticator-app",
17
+ confirm: "Bevestigen",
18
+ nothingToConfirmWith: "Voeg eerst een passkey of een authenticator-app toe op je accountpagina."
19
+ }
20
+ });
21
+ //#endregion
22
+ export { text };
@@ -0,0 +1,88 @@
1
+ import { useOptionalAuth } from "../auth/auth-provider.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { OtpInput } from "../primitives/otp-input.js";
4
+ import { text } from "./confirm-sign-in-messages.js";
5
+ import { useState } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { useMutation, useQuery } from "@tanstack/react-query";
8
+ //#region src/app/confirm-sign-in.tsx
9
+ /**
10
+ * Asks the person to confirm it's them, with a passkey or their authenticator app, after the server refused something
11
+ * that needs a recent strong sign-in (`strong-sign-in-required`). It offers only what the account has; a passkey signs
12
+ * the browser in anew, a code confirms the session it has.
13
+ */
14
+ const ConfirmSignIn = ({ minutes, onConfirmed }) => {
15
+ const auth = useOptionalAuth();
16
+ const [code, setCode] = useState("");
17
+ const methods = useQuery({
18
+ queryKey: ["account-sign-in-methods"],
19
+ queryFn: () => auth.session.account.signInMethods(),
20
+ enabled: Boolean(auth)
21
+ });
22
+ const passkey = useMutation({
23
+ mutationFn: () => auth.signInWithPasskey(),
24
+ onSuccess: onConfirmed
25
+ });
26
+ const authenticator = useMutation({
27
+ mutationFn: (entered) => auth.verifyTwoFactor({
28
+ method: "totp",
29
+ code: entered
30
+ }),
31
+ onSuccess: onConfirmed
32
+ });
33
+ const account = methods.data?.account;
34
+ const error = passkey.error ?? authenticator.error ?? methods.error;
35
+ return /* @__PURE__ */ jsxs("section", {
36
+ "aria-label": text("confirmTitle"),
37
+ className: "mt-2 flex flex-col gap-2 rounded-md border border-border bg-surface p-3",
38
+ children: [
39
+ /* @__PURE__ */ jsx("p", {
40
+ className: "font-medium",
41
+ children: text("confirmTitle")
42
+ }),
43
+ /* @__PURE__ */ jsx("p", {
44
+ className: "text-muted-foreground",
45
+ children: text("confirmWhy", minutes)
46
+ }),
47
+ account && account.passkeys > 0 && /* @__PURE__ */ jsx(Button, {
48
+ variant: "primary",
49
+ size: "sm",
50
+ className: "self-start",
51
+ loading: passkey.isPending,
52
+ onClick: () => passkey.mutate(),
53
+ children: text("usePasskey")
54
+ }),
55
+ account?.authenticatorApp && /* @__PURE__ */ jsxs("form", {
56
+ className: "flex flex-wrap items-end gap-2",
57
+ onSubmit: (event) => {
58
+ event.preventDefault();
59
+ authenticator.mutate(code.trim());
60
+ },
61
+ children: [/* @__PURE__ */ jsxs("label", {
62
+ className: "flex flex-col gap-1.5",
63
+ children: [text("authenticatorCode"), /* @__PURE__ */ jsx(OtpInput, {
64
+ name: "code",
65
+ required: true,
66
+ value: code,
67
+ invalid: authenticator.isError,
68
+ onChange: setCode,
69
+ onComplete: (entered) => authenticator.mutate(entered)
70
+ })]
71
+ }), /* @__PURE__ */ jsx(Button, {
72
+ type: "submit",
73
+ size: "sm",
74
+ loading: authenticator.isPending,
75
+ children: text("confirm")
76
+ })]
77
+ }),
78
+ account && account.passkeys === 0 && !account.authenticatorApp && /* @__PURE__ */ jsx("p", { children: text("nothingToConfirmWith") }),
79
+ error && /* @__PURE__ */ jsx("p", {
80
+ role: "alert",
81
+ className: "font-medium text-danger-text",
82
+ children: error.message
83
+ })
84
+ ]
85
+ });
86
+ };
87
+ //#endregion
88
+ export { ConfirmSignIn };
@@ -6,15 +6,19 @@ import { displayValue } from "../live/display-value.js";
6
6
  import { text } from "./messages.js";
7
7
  import { toDraft } from "../live/field-values.js";
8
8
  import { ErrorBanner } from "./error-banner.js";
9
+ import { ConfirmSignIn } from "./confirm-sign-in.js";
9
10
  import { useState } from "react";
10
11
  import { Eye, EyeOff } from "lucide-react";
12
+ import { isApiError } from "@protobase/client";
11
13
  import { jsx, jsxs } from "react/jsx-runtime";
12
14
  import { useMutation } from "@tanstack/react-query";
13
15
  //#region src/app/sensitive-field.tsx
14
16
  const mask = "••••••••••";
17
+ const strongSignInMinutes = (error) => isApiError(error) && error.slug === "strong-sign-in-required" ? error.problem.minutes ?? 15 : void 0;
15
18
  /**
16
19
  * A value hidden like a password. Showing a stored one asks the server for it, once per page visit; a write-only one has
17
- * none to show, so its eye only unmasks what was typed.
20
+ * none to show, so its eye only unmasks what was typed. When the server wants a recent strong sign-in first, the field
21
+ * asks the person to confirm it's them, then shows the value.
18
22
  */
19
23
  const SensitiveField = ({ field, hints, value, reveal, locked, invalid, placeholder, onChange }) => {
20
24
  const [shown, setShown] = useState(false);
@@ -38,7 +42,11 @@ const SensitiveField = ({ field, hints, value, reveal, locked, invalid, placehol
38
42
  onClick: toggle,
39
43
  children: !stored.isPending && (shown ? /* @__PURE__ */ jsx(EyeOff, { className: "size-3.5" }) : /* @__PURE__ */ jsx(Eye, { className: "size-3.5" }))
40
44
  });
41
- const error = stored.error && /* @__PURE__ */ jsx("div", {
45
+ const minutes = strongSignInMinutes(stored.error);
46
+ const error = minutes !== void 0 ? /* @__PURE__ */ jsx(ConfirmSignIn, {
47
+ minutes,
48
+ onConfirmed: () => stored.mutate()
49
+ }) : stored.error && /* @__PURE__ */ jsx("div", {
42
50
  className: "mt-2",
43
51
  children: /* @__PURE__ */ jsx(ErrorBanner, {
44
52
  title: text("couldNotShow", label),
@@ -12,6 +12,8 @@ const useAuth = () => {
12
12
  if (!api) throw new Error("useAuth must be used inside AuthProvider");
13
13
  return api;
14
14
  };
15
+ /** The auth state and steps inside `AuthProvider`, `null` outside it, for a part that also works without one. */
16
+ const useOptionalAuth = () => useContext(AuthContext);
15
17
  /**
16
18
  * Finds out whether the server has a login and who is signed in, and keeps that state for the app. Without a `session`,
17
19
  * it makes one on `origin`, the page's own by default; in the iOS or Android app, with its `nativeBridge`, the session
@@ -216,4 +218,4 @@ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
216
218
  });
217
219
  };
218
220
  //#endregion
219
- export { AuthProvider, useAuth };
221
+ export { AuthProvider, useAuth, useOptionalAuth };
@@ -1,7 +1,7 @@
1
1
  import { text } from "./messages.js";
2
2
  import { Button } from "../primitives/button.js";
3
- import { AuthLayout } from "./auth-layout.js";
4
3
  import { OtpInput } from "../primitives/otp-input.js";
4
+ import { AuthLayout } from "./auth-layout.js";
5
5
  import { ResendCodeButton } from "./resend-code-button.js";
6
6
  import { useEffect, useRef, useState } from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -1,9 +1,9 @@
1
1
  import { text } from "./messages.js";
2
2
  import { Button } from "../primitives/button.js";
3
3
  import { Input } from "../primitives/input.js";
4
+ import { OtpInput } from "../primitives/otp-input.js";
4
5
  import { Checkbox } from "../primitives/checkbox.js";
5
6
  import { AuthLayout } from "./auth-layout.js";
6
- import { OtpInput } from "../primitives/otp-input.js";
7
7
  import { useState } from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/auth/two-factor-form.tsx
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { AppLink, useAppAddress } from "./app/use-app-address.js";
12
12
  import { Segmented } from "./primitives/segmented.js";
13
13
  import { Textarea } from "./primitives/textarea.js";
14
14
  import { useList } from "./data/use-list.js";
15
+ import { OtpInput } from "./primitives/otp-input.js";
15
16
  import { definePlugin, defineUi } from "./app/pages/project-ui.js";
16
17
  import { Markdown } from "./markdown/markdown.js";
17
18
  import { ActionInputError } from "./app/pages/action-inputs/input-errors.js";
@@ -22,7 +23,6 @@ import { SignInPrompt } from "./auth/sign-in-prompt.js";
22
23
  import { useListDisplay } from "./app/displays/display-context.js";
23
24
  import { MenuSelect } from "./primitives/menu-select.js";
24
25
  import { AuthLayout } from "./auth/auth-layout.js";
25
- import { OtpInput } from "./primitives/otp-input.js";
26
26
  import { allowDelayMs, isFramed, useArmedAfterFocus, useGuardedPress } from "./connect/interaction-guard.js";
27
27
  import { ResendCodeButton } from "./auth/resend-code-button.js";
28
28
  import { CodeForm } from "./auth/code-form.js";
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.46.8";
2
+ var version = "0.46.9";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };