@sjolystinnovation/app-kit 0.22.0 → 1.0.1

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 (52) hide show
  1. package/README.md +1280 -772
  2. package/package.json +21 -4
  3. package/src/admin/AdminUsersPanel.tsx +85 -35
  4. package/src/admin/CompanySettingsForm.tsx +33 -13
  5. package/src/admin/EmailStatsPanel.tsx +76 -0
  6. package/src/admin/LegalPagesEditor.tsx +41 -14
  7. package/src/admin/adminUsersPanelStrings.ts +67 -0
  8. package/src/admin/companySettingsFormStrings.ts +26 -0
  9. package/src/admin/emailStatsPanelStrings.ts +29 -0
  10. package/src/admin/index.ts +14 -5
  11. package/src/admin/legalPagesEditorStrings.ts +25 -0
  12. package/src/api/apiErrorCode.ts +28 -0
  13. package/src/api/apiFieldErrors.ts +82 -11
  14. package/src/api/failureBody.ts +10 -0
  15. package/src/api/fieldErrorText.ts +32 -0
  16. package/src/api/index.ts +5 -1
  17. package/src/api/legalPage.ts +10 -0
  18. package/src/api/request.ts +13 -3
  19. package/src/api/services.ts +42 -12
  20. package/src/auth/createAuthOptions.ts +9 -3
  21. package/src/branding/BrandingManager.tsx +31 -36
  22. package/src/branding/brandingManagerStrings.ts +49 -0
  23. package/src/branding/index.ts +3 -3
  24. package/src/legal/LegalPageView.tsx +2 -17
  25. package/src/legal/index.ts +4 -1
  26. package/src/legal/keys.ts +10 -0
  27. package/src/legal/legalPageViewStrings.ts +17 -0
  28. package/src/locales/nb.ts +330 -0
  29. package/src/sections/SectionRenderer.tsx +275 -0
  30. package/src/sections/SectionsEditor.tsx +151 -0
  31. package/src/sections/SmartLink.tsx +42 -0
  32. package/src/sections/editors.tsx +311 -0
  33. package/src/sections/factory.ts +26 -0
  34. package/src/sections/index.ts +69 -0
  35. package/src/sections/sectionsEditorStrings.ts +108 -0
  36. package/src/sections/types.ts +107 -0
  37. package/src/seo/index.ts +1 -0
  38. package/src/seo/site.ts +5 -5
  39. package/src/session/expiry.ts +3 -1
  40. package/src/session/index.ts +0 -1
  41. package/src/session/react/CredentialsForm.tsx +8 -2
  42. package/src/session/react/credentialsFormStrings.ts +4 -1
  43. package/src/session/react/index.ts +1 -1
  44. package/src/styles.css +2 -0
  45. package/src/testing/index.ts +1 -9
  46. package/src/ui/ErrorState.tsx +1 -15
  47. package/src/ui/NotFoundState.tsx +1 -13
  48. package/src/ui/ResetPasswordFlow.tsx +55 -12
  49. package/src/ui/errorStateStrings.ts +16 -0
  50. package/src/ui/index.ts +10 -6
  51. package/src/ui/notFoundStateStrings.ts +14 -0
  52. package/src/ui/resetPasswordFlowStrings.ts +53 -4
@@ -5,6 +5,5 @@ export {
5
5
  getSessionPromptOpen,
6
6
  isTerminalSessionError,
7
7
  openSessionPrompt,
8
- subscribeSessionPrompt,
9
8
  } from "./expiry";
10
9
  export type { SessionError } from "./expiry";
@@ -17,7 +17,7 @@ import { withDefaults, type TextOverrides } from "../../ui/withDefaults";
17
17
  /** Props for `CredentialsForm`. */
18
18
  export interface CredentialsFormProps {
19
19
  initialEmail?: string | undefined;
20
- /** A function that runs after next-auth accepts the credentials. Throw `SignInRejected` from it to refuse with a reason. */
20
+ /** A function that runs after next-auth accepts the credentials. If it throws, the form shows its generic error. */
21
21
  onSignedIn: () => void | Promise<void>;
22
22
  /** The extra buttons next to the submit button, such as a close button. */
23
23
  children?: ReactNode;
@@ -26,6 +26,12 @@ export interface CredentialsFormProps {
26
26
  onPendingChange?: ((pending: boolean) => void) | undefined;
27
27
  }
28
28
 
29
+ // The form's text for each sign-in error. Any other error reads as wrong credentials.
30
+ function signInErrorText(error: string, text: CredentialsFormStrings): string {
31
+ if (error === SIGN_IN_ERRORS.unavailable) return text.signInUnavailable;
32
+ return error === SIGN_IN_ERRORS.lockedOut ? text.lockedOut : text.invalidCredentials;
33
+ }
34
+
29
35
  // next-auth answers a stale CSRF token with its own sign-in page and ?csrf=true.
30
36
  function refusedForCsrf(url: string | null | undefined): boolean {
31
37
  if (!url) return false;
@@ -58,7 +64,7 @@ export function CredentialsForm({
58
64
  try {
59
65
  const result = await signIn("credentials", { email, password, redirect: false });
60
66
  if (result?.error) {
61
- setError(result.error === SIGN_IN_ERRORS.unavailable ? text.signInUnavailable : text.invalidCredentials);
67
+ setError(signInErrorText(result.error, text));
62
68
  return;
63
69
  }
64
70
  // next-auth reports a stale CSRF token as a success, see refusedForCsrf.
@@ -1,6 +1,6 @@
1
1
  // Plain values, with no "use client", so a server component can import them.
2
2
 
3
- /** Thrown from `onSignedIn` to refuse a sign-in. The form shows its message. */
3
+ /** Thrown inside app-kit from `onSignedIn` to refuse a sign-in. The form shows its message. Not exported. */
4
4
  export class SignInRejected extends Error {}
5
5
 
6
6
  /** The form's text. Pass the app's own language. The defaults are English. */
@@ -10,6 +10,8 @@ export interface CredentialsFormStrings {
10
10
  signIn: string;
11
11
  signingIn: string;
12
12
  invalidCredentials: string;
13
+ /** The text for a login the API has locked after too many failed sign-ins. */
14
+ lockedOut: string;
13
15
  signInUnavailable: string;
14
16
  somethingWentWrong: string;
15
17
  showPassword: string;
@@ -22,6 +24,7 @@ export const defaultCredentialsFormStrings: CredentialsFormStrings = {
22
24
  signIn: "Sign in",
23
25
  signingIn: "Signing in…",
24
26
  invalidCredentials: "Invalid email or password.",
27
+ lockedOut: "Too many failed attempts. This login is temporarily locked, try again later.",
25
28
  signInUnavailable: "Sign-in is unavailable right now. Try again shortly.",
26
29
  somethingWentWrong: "Something went wrong",
27
30
  showPassword: "Show password",
@@ -10,7 +10,7 @@ export { RoleGate } from "./RoleGate";
10
10
  export type { RoleGateProps } from "./RoleGate";
11
11
  export { CredentialsForm } from "./CredentialsForm";
12
12
  export type { CredentialsFormProps } from "./CredentialsForm";
13
- export { SignInRejected, defaultCredentialsFormStrings } from "./credentialsFormStrings";
13
+ export { defaultCredentialsFormStrings } from "./credentialsFormStrings";
14
14
  export type { CredentialsFormStrings } from "./credentialsFormStrings";
15
15
  export { LoginPage } from "./LoginPage";
16
16
  export type { LoginPageProps } from "./LoginPage";
package/src/styles.css CHANGED
@@ -90,6 +90,8 @@
90
90
  --color-admin-tab-indicator: var(--color-primary);
91
91
  --color-chart-grid: var(--color-gray-200);
92
92
  --color-chart-label: var(--color-gray-400);
93
+ --color-admin-stat-label: var(--color-gray-500);
94
+ --color-admin-stat-value: var(--color-gray-700);
93
95
 
94
96
  --radius-button: var(--radius-lg);
95
97
  --color-button-primary-bg: var(--color-primary);
@@ -94,20 +94,12 @@ export function toastMock() {
94
94
  return { toast, Toaster: () => null };
95
95
  }
96
96
 
97
- /** The HTTP calls of an app's axios instance as spies. */
98
- export const axiosSpies = { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn() };
99
-
100
- /** Builds a module whose default export is an axios instance, such as the app's `services/axiosInstance`. */
101
- export function axiosInstanceMock() {
102
- return { default: axiosSpies };
103
- }
104
-
105
97
  /** Signs out, goes back to `/`, and clears every spy's calls and answers. */
106
98
  export function resetTestMocks(): void {
107
99
  setSession(null);
108
100
  navigation.pathname = "/";
109
101
  navigation.searchParams = new URLSearchParams();
110
- for (const spy of [auth.signIn, auth.signOut, auth.getSession, auth.update, ...Object.values(navigation.router), ...Object.values(toastSpies), ...Object.values(axiosSpies)]) {
102
+ for (const spy of [auth.signIn, auth.signOut, auth.getSession, auth.update, ...Object.values(navigation.router), ...Object.values(toastSpies)]) {
111
103
  spy.mockReset();
112
104
  }
113
105
  }
@@ -2,23 +2,9 @@
2
2
 
3
3
  import Link from "next/link";
4
4
  import { cx } from "./cx";
5
+ import { defaultErrorStateStrings, type ErrorStateStrings } from "./errorStateStrings";
5
6
  import { withDefaults, type TextOverrides } from "./withDefaults";
6
7
 
7
- /** The error page's text. Pass the app's own language. The defaults are English. */
8
- export interface ErrorStateStrings {
9
- title: string;
10
- body: string;
11
- tryAgain: string;
12
- home: string;
13
- }
14
-
15
- export const defaultErrorStateStrings: ErrorStateStrings = {
16
- title: "Temporarily unavailable",
17
- body: "We can't reach the service right now. Please try again shortly.",
18
- tryAgain: "Try again",
19
- home: "Go to the front page",
20
- };
21
-
22
8
  /** Props for `ErrorState`. */
23
9
  export interface ErrorStateProps {
24
10
  /** The `reset` Next passes to an `error.tsx`. It renders the page again. */
@@ -1,20 +1,8 @@
1
1
  import Link from "next/link";
2
2
  import { cx } from "./cx";
3
+ import { defaultNotFoundStateStrings, type NotFoundStateStrings } from "./notFoundStateStrings";
3
4
  import { withDefaults, type TextOverrides } from "./withDefaults";
4
5
 
5
- /** The not-found page's text. Pass the app's own language. The defaults are English. */
6
- export interface NotFoundStateStrings {
7
- code: string;
8
- body: string;
9
- home: string;
10
- }
11
-
12
- export const defaultNotFoundStateStrings: NotFoundStateStrings = {
13
- code: "404",
14
- body: "This page does not exist.",
15
- home: "Go home",
16
- };
17
-
18
6
  /** Props for `NotFoundState`. */
19
7
  export interface NotFoundStateProps {
20
8
  /** The home link's target, such as `/` or `/en`. */
@@ -2,11 +2,17 @@
2
2
 
3
3
  import Link from "next/link";
4
4
  import { useState, type FormEvent } from "react";
5
+ import { errorText, fieldErrorTexts } from "../api/fieldErrorText";
5
6
  import { passwordProblems, type PasswordStrings } from "../validation/passwordRules";
6
7
  import { Alert } from "./Alert";
7
8
  import { authLinkClassName, authPageClassName, authTitleClassName } from "./authClasses";
8
9
  import { PasswordInput } from "./PasswordInput";
9
- import { defaultResetPasswordFlowStrings, type ResetPasswordFlowStrings } from "./resetPasswordFlowStrings";
10
+ import {
11
+ defaultResetPasswordFlowStrings,
12
+ type ResetPasswordErrorStrings,
13
+ type ResetPasswordFlowStrings,
14
+ type ResetPasswordValidationStrings,
15
+ } from "./resetPasswordFlowStrings";
10
16
  import { TextInput } from "./TextInput";
11
17
  import { withDefaults, type TextOverrides } from "./withDefaults";
12
18
 
@@ -22,20 +28,30 @@ export interface ResetPasswordFlowProps {
22
28
  initialEmail?: string | undefined;
23
29
  /** The code from the emailed link. A code opens the flow on its last step. */
24
30
  initialCode?: string | undefined;
25
- strings?: TextOverrides<ResetPasswordFlowStrings> | undefined;
31
+ /** The flow's text. `validation` and `errors` may name only some codes, the rest keep their defaults. */
32
+ strings?:
33
+ | (TextOverrides<Omit<ResetPasswordFlowStrings, "validation" | "errors">> & {
34
+ validation?: TextOverrides<ResetPasswordValidationStrings> | undefined;
35
+ errors?: TextOverrides<ResetPasswordErrorStrings> | undefined;
36
+ })
37
+ | undefined;
26
38
  /** The classes that replace the page's outer spacing, such as when the flow sits inside the app's own card. */
27
39
  className?: string | undefined;
28
40
  /** The password rule messages, for the app's own language. */
29
41
  passwordStrings?: TextOverrides<PasswordStrings> | undefined;
30
42
  }
31
43
 
44
+ // The fields each step shows. A field error for any other field goes in the message above the form.
45
+ const STEP_FIELDS = { 1: ["email"], 2: ["email", "code", "newPassword"] } as const;
46
+
32
47
  const button =
33
48
  "w-full rounded-button bg-button-primary-bg text-button-primary-text font-semibold py-2.5 hover:brightness-95 disabled:opacity-60 transition";
34
49
 
35
50
  /**
36
51
  * Renders password reset in two steps: the email asks for a code, then the code and a new password
37
52
  * set it. The flow checks the new password against the API's rules before it sends it. A link with
38
- * the email and code, such as an invite, opens on the second step.
53
+ * the email and code, such as an invite, opens on the second step. The API's refusals show in the
54
+ * app's language through `validation` and `errors`, each field's under its field.
39
55
  */
40
56
  export function ResetPasswordFlow({
41
57
  requestReset,
@@ -47,26 +63,45 @@ export function ResetPasswordFlow({
47
63
  className = authPageClassName,
48
64
  passwordStrings,
49
65
  }: ResetPasswordFlowProps) {
50
- const text = withDefaults(defaultResetPasswordFlowStrings, strings);
66
+ const { validation: validationOverrides, errors: errorOverrides, ...textOverrides } = strings ?? {};
67
+ const text = withDefaults(defaultResetPasswordFlowStrings, textOverrides);
68
+ const validation = withDefaults(defaultResetPasswordFlowStrings.validation, validationOverrides);
69
+ const errors = withDefaults(defaultResetPasswordFlowStrings.errors, errorOverrides);
51
70
  const [step, setStep] = useState<1 | 2>(initialCode ? 2 : 1);
52
71
  const [email, setEmail] = useState(initialEmail);
53
72
  const [code, setCode] = useState(initialCode);
54
73
  const [newPassword, setNewPassword] = useState("");
55
74
  const [error, setError] = useState<string | null>(null);
75
+ // The texts for the fields the last attempt got wrong, keyed by field name.
76
+ const [fieldErrors, setFieldErrors] = useState<Record<string, string[]>>({});
56
77
  const [done, setDone] = useState<string | null>(null);
57
78
  const [loading, setLoading] = useState(false);
58
79
 
59
- const failure = (err: unknown) => (err instanceof Error && err.message ? err.message : text.somethingWentWrong);
80
+ // Field errors go under the fields this step shows, and the message holds the rest.
81
+ const fail = (err: unknown, shown: readonly string[]) => {
82
+ const texts = Object.entries(fieldErrorTexts(err, validation));
83
+ if (texts.length === 0) {
84
+ setError(errorText(err, errors, text.somethingWentWrong));
85
+ return;
86
+ }
87
+ // Only the fields this step shows render their errors.
88
+ setFieldErrors(Object.fromEntries(texts));
89
+ // Empty when every field error has its field, which shows no message.
90
+ setError(texts.filter(([field]) => !shown.includes(field)).flatMap(([, list]) => list).join(" "));
91
+ };
92
+
93
+ const fieldError = (field: string) => fieldErrors[field]?.join(" ");
60
94
 
61
95
  const sendCode = async (event: FormEvent) => {
62
96
  event.preventDefault();
63
97
  setError(null);
98
+ setFieldErrors({});
64
99
  setLoading(true);
65
100
  try {
66
101
  await requestReset({ email });
67
102
  setStep(2);
68
103
  } catch (err) {
69
- setError(failure(err));
104
+ fail(err, STEP_FIELDS[1]);
70
105
  } finally {
71
106
  setLoading(false);
72
107
  }
@@ -75,9 +110,10 @@ export function ResetPasswordFlow({
75
110
  const setPassword = async (event: FormEvent) => {
76
111
  event.preventDefault();
77
112
  setError(null);
113
+ setFieldErrors({});
78
114
  const problems = passwordProblems(newPassword, passwordStrings);
79
115
  if (problems.length > 0) {
80
- setError(text.passwordInvalid ?? problems.join(" "));
116
+ setFieldErrors({ newPassword: text.passwordInvalid === null ? problems : [text.passwordInvalid] });
81
117
  return;
82
118
  }
83
119
  setLoading(true);
@@ -86,7 +122,7 @@ export function ResetPasswordFlow({
86
122
  const message = (result as { message?: unknown } | null | undefined)?.message;
87
123
  setDone(text.done ?? (typeof message === "string" ? message : ""));
88
124
  } catch (err) {
89
- setError(failure(err));
125
+ fail(err, STEP_FIELDS[2]);
90
126
  } finally {
91
127
  setLoading(false);
92
128
  }
@@ -109,7 +145,7 @@ export function ResetPasswordFlow({
109
145
  <form onSubmit={sendCode} className="space-y-4">
110
146
  {error && <Alert variant="error">{error}</Alert>}
111
147
  {text.intro && <p className="text-sm text-auth-text">{text.intro}</p>}
112
- <TextInput label={text.email} name="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
148
+ <TextInput label={text.email} name="email" type="email" error={fieldError("email")} value={email} onChange={(e) => setEmail(e.target.value)} required />
113
149
  <button type="submit" disabled={loading} className={button}>
114
150
  {loading ? text.requestingCode : text.requestCode}
115
151
  </button>
@@ -118,9 +154,16 @@ export function ResetPasswordFlow({
118
154
  <form onSubmit={setPassword} className="space-y-4">
119
155
  {text.codeSent && <p className="text-sm text-auth-note">{text.codeSent}</p>}
120
156
  {error && <Alert variant="error">{error}</Alert>}
121
- <TextInput label={text.email} name="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
122
- <TextInput label={text.code} name="code" value={code} onChange={(e) => setCode(e.target.value)} required />
123
- <PasswordInput label={text.newPassword} name="newPassword" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} required />
157
+ <TextInput label={text.email} name="email" type="email" error={fieldError("email")} value={email} onChange={(e) => setEmail(e.target.value)} required />
158
+ <TextInput label={text.code} name="code" error={fieldError("code")} value={code} onChange={(e) => setCode(e.target.value)} required />
159
+ <PasswordInput
160
+ label={text.newPassword}
161
+ name="newPassword"
162
+ error={fieldError("newPassword")}
163
+ value={newPassword}
164
+ onChange={(e) => setNewPassword(e.target.value)}
165
+ required
166
+ />
124
167
  {text.passwordHint && <p className="text-xs text-auth-hint">{text.passwordHint}</p>}
125
168
  <button type="submit" disabled={loading} className={button}>
126
169
  {loading ? text.settingPassword : text.setPassword}
@@ -0,0 +1,16 @@
1
+ // Plain values, with no "use client", so a server component can import them.
2
+
3
+ /** The error page's text. Pass the app's own language. The defaults are English. */
4
+ export interface ErrorStateStrings {
5
+ title: string;
6
+ body: string;
7
+ tryAgain: string;
8
+ home: string;
9
+ }
10
+
11
+ export const defaultErrorStateStrings: ErrorStateStrings = {
12
+ title: "Temporarily unavailable",
13
+ body: "We can't reach the service right now. Please try again shortly.",
14
+ tryAgain: "Try again",
15
+ home: "Go to the front page",
16
+ };
package/src/ui/index.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  export { Alert } from "./Alert";
2
2
  export type { AlertProps } from "./Alert";
3
- export { ContentImage, CONTENT_IMAGE_WIDTHS, contentImagePath, contentImageSrcSet } from "./ContentImage";
3
+ export { ContentImage, contentImagePath, contentImageSrcSet } from "./ContentImage";
4
4
  export type { ContentImageProps } from "./ContentImage";
5
- export { ErrorState, defaultErrorStateStrings } from "./ErrorState";
6
- export type { ErrorStateProps, ErrorStateStrings } from "./ErrorState";
7
- export { NotFoundState, defaultNotFoundStateStrings } from "./NotFoundState";
8
- export type { NotFoundStateProps, NotFoundStateStrings } from "./NotFoundState";
5
+ export { ErrorState } from "./ErrorState";
6
+ export type { ErrorStateProps } from "./ErrorState";
7
+ export { defaultErrorStateStrings } from "./errorStateStrings";
8
+ export type { ErrorStateStrings } from "./errorStateStrings";
9
+ export { NotFoundState } from "./NotFoundState";
10
+ export type { NotFoundStateProps } from "./NotFoundState";
11
+ export { defaultNotFoundStateStrings } from "./notFoundStateStrings";
12
+ export type { NotFoundStateStrings } from "./notFoundStateStrings";
9
13
  export { PasswordInput } from "./PasswordInput";
10
14
  export type { PasswordInputProps } from "./PasswordInput";
11
15
  export { ResetPasswordFlow } from "./ResetPasswordFlow";
12
16
  export type { ResetPasswordFlowProps } from "./ResetPasswordFlow";
13
17
  export { defaultResetPasswordFlowStrings } from "./resetPasswordFlowStrings";
14
- export type { ResetPasswordFlowStrings } from "./resetPasswordFlowStrings";
18
+ export type { ResetPasswordErrorStrings, ResetPasswordFlowStrings, ResetPasswordValidationStrings } from "./resetPasswordFlowStrings";
15
19
  export { TextArea } from "./TextArea";
16
20
  export type { TextAreaProps } from "./TextArea";
17
21
  export { TextInput } from "./TextInput";
@@ -0,0 +1,14 @@
1
+ // Plain values, with no "use client", so a server component can import them.
2
+
3
+ /** The not-found page's text. Pass the app's own language. The defaults are English. */
4
+ export interface NotFoundStateStrings {
5
+ code: string;
6
+ body: string;
7
+ home: string;
8
+ }
9
+
10
+ export const defaultNotFoundStateStrings: NotFoundStateStrings = {
11
+ code: "404",
12
+ body: "This page does not exist.",
13
+ home: "Go home",
14
+ };
@@ -1,5 +1,37 @@
1
1
  // Plain values, with no "use client", so a server component can import them.
2
2
 
3
+ import { PASSWORD_MIN_LENGTH } from "../validation/passwordRules";
4
+
5
+ // The rules passwordProblems checks, for the hint and the weak password text.
6
+ const passwordRules = `Use at least ${PASSWORD_MIN_LENGTH} characters, with at least one lowercase and one uppercase letter from A to Z and one digit from 0 to 9.`;
7
+
8
+ /**
9
+ * The flow's text for each field error code of SjolystInnovation.AppKit.Auth's reset endpoints,
10
+ * shown under the email, code or new password field. `{max}` in `passwordTooShort` becomes the
11
+ * API's least number of characters. The flow shows the API's own message for a code that is not
12
+ * here, such as `weakPassword` from a password rule the app adds.
13
+ */
14
+ export interface ResetPasswordValidationStrings {
15
+ required: string;
16
+ invalidEmail: string;
17
+ passwordTooShort: string;
18
+ passwordRequiresDigit: string;
19
+ passwordRequiresLower: string;
20
+ passwordRequiresUpper: string;
21
+ passwordRequiresNonAlphanumeric: string;
22
+ }
23
+
24
+ /**
25
+ * The flow's text for each code of SjolystInnovation.AppKit.Auth's reset endpoints that is not
26
+ * about one field. The flow shows the API's own message for a code that is not here.
27
+ */
28
+ export interface ResetPasswordErrorStrings {
29
+ /** The code is wrong or expired, or the email has no active account. */
30
+ invalidResetCode: string;
31
+ /** Five wrong codes in a row. The user has to ask for a new code. */
32
+ tooManyResetAttempts: string;
33
+ }
34
+
3
35
  /**
4
36
  * The reset flow's text. Pass the app's own language. The defaults are English. A text that is null
5
37
  * is left out of the page.
@@ -29,6 +61,10 @@ export interface ResetPasswordFlowStrings {
29
61
  /** The text shown for a password the rules refuse. Null lists each rule it breaks. */
30
62
  passwordInvalid: string | null;
31
63
  somethingWentWrong: string;
64
+ /** The field error texts, by code. Any code left out keeps its default. */
65
+ validation: ResetPasswordValidationStrings;
66
+ /** The texts for the other refusals, by code. Any code left out keeps its default. */
67
+ errors: ResetPasswordErrorStrings;
32
68
  }
33
69
 
34
70
  export const defaultResetPasswordFlowStrings: ResetPasswordFlowStrings = {
@@ -36,17 +72,30 @@ export const defaultResetPasswordFlowStrings: ResetPasswordFlowStrings = {
36
72
  signInPrompt: "Remembered it?",
37
73
  signIn: "Sign in",
38
74
  backToSignIn: "Back to sign in →",
39
- intro: null,
75
+ intro: "Enter your email address and we will send you a code.",
40
76
  codeSent: "Check your email for the reset code.",
41
77
  email: "Email",
42
78
  code: "Reset code",
43
79
  newPassword: "New password",
44
- passwordHint: null,
80
+ passwordHint: passwordRules,
45
81
  requestCode: "Send reset code",
46
82
  requestingCode: "Sending…",
47
83
  setPassword: "Reset password",
48
84
  settingPassword: "Resetting…",
49
- done: null,
50
- passwordInvalid: null,
85
+ done: "Your password has been changed. You can sign in now.",
86
+ passwordInvalid: `The password does not meet the requirements. ${passwordRules}`,
51
87
  somethingWentWrong: "Something went wrong",
88
+ validation: {
89
+ required: "Required.",
90
+ invalidEmail: "Not a valid email address.",
91
+ passwordTooShort: "Passwords must be at least {max} characters.",
92
+ passwordRequiresDigit: "Passwords must have at least one digit ('0'-'9').",
93
+ passwordRequiresLower: "Passwords must have at least one lowercase ('a'-'z').",
94
+ passwordRequiresUpper: "Passwords must have at least one uppercase ('A'-'Z').",
95
+ passwordRequiresNonAlphanumeric: "Passwords must have at least one non alphanumeric character.",
96
+ },
97
+ errors: {
98
+ invalidResetCode: "Invalid or expired reset code.",
99
+ tooManyResetAttempts: "Too many attempts. Request a new reset code.",
100
+ },
52
101
  };