@sjolystinnovation/app-kit 0.22.0 → 1.0.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/README.md +1281 -773
- package/package.json +21 -4
- package/src/admin/AdminUsersPanel.tsx +85 -35
- package/src/admin/CompanySettingsForm.tsx +33 -13
- package/src/admin/EmailStatsPanel.tsx +76 -0
- package/src/admin/LegalPagesEditor.tsx +41 -14
- package/src/admin/adminUsersPanelStrings.ts +67 -0
- package/src/admin/companySettingsFormStrings.ts +26 -0
- package/src/admin/emailStatsPanelStrings.ts +29 -0
- package/src/admin/index.ts +14 -5
- package/src/admin/legalPagesEditorStrings.ts +25 -0
- package/src/api/apiErrorCode.ts +28 -0
- package/src/api/apiFieldErrors.ts +82 -11
- package/src/api/failureBody.ts +10 -0
- package/src/api/fieldErrorText.ts +32 -0
- package/src/api/index.ts +5 -1
- package/src/api/legalPage.ts +10 -0
- package/src/api/request.ts +13 -3
- package/src/api/services.ts +42 -12
- package/src/auth/createAuthOptions.ts +9 -3
- package/src/branding/BrandingManager.tsx +31 -36
- package/src/branding/brandingManagerStrings.ts +49 -0
- package/src/branding/index.ts +3 -3
- package/src/legal/LegalPageView.tsx +2 -17
- package/src/legal/index.ts +4 -1
- package/src/legal/keys.ts +10 -0
- package/src/legal/legalPageViewStrings.ts +17 -0
- package/src/locales/nb.ts +330 -0
- package/src/sections/SectionRenderer.tsx +275 -0
- package/src/sections/SectionsEditor.tsx +151 -0
- package/src/sections/SmartLink.tsx +42 -0
- package/src/sections/editors.tsx +311 -0
- package/src/sections/factory.ts +26 -0
- package/src/sections/index.ts +69 -0
- package/src/sections/sectionsEditorStrings.ts +108 -0
- package/src/sections/types.ts +107 -0
- package/src/seo/index.ts +1 -0
- package/src/seo/site.ts +5 -5
- package/src/session/expiry.ts +3 -1
- package/src/session/index.ts +0 -1
- package/src/session/react/CredentialsForm.tsx +8 -2
- package/src/session/react/credentialsFormStrings.ts +4 -1
- package/src/session/react/index.ts +1 -1
- package/src/styles.css +2 -0
- package/src/testing/index.ts +1 -9
- package/src/ui/ErrorState.tsx +1 -15
- package/src/ui/NotFoundState.tsx +1 -13
- package/src/ui/ResetPasswordFlow.tsx +55 -12
- package/src/ui/errorStateStrings.ts +16 -0
- package/src/ui/index.ts +10 -6
- package/src/ui/notFoundStateStrings.ts +14 -0
- package/src/ui/resetPasswordFlowStrings.ts +44 -0
package/src/session/index.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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 {
|
|
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);
|
package/src/testing/index.ts
CHANGED
|
@@ -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)
|
|
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
|
}
|
package/src/ui/ErrorState.tsx
CHANGED
|
@@ -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. */
|
package/src/ui/NotFoundState.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
3
|
+
export { ContentImage, contentImagePath, contentImageSrcSet } from "./ContentImage";
|
|
4
4
|
export type { ContentImageProps } from "./ContentImage";
|
|
5
|
-
export { ErrorState
|
|
6
|
-
export type { ErrorStateProps
|
|
7
|
-
export {
|
|
8
|
-
export type {
|
|
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,32 @@
|
|
|
1
1
|
// Plain values, with no "use client", so a server component can import them.
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The flow's text for each field error code of SjolystInnovation.AppKit.Auth's reset endpoints,
|
|
5
|
+
* shown under the email, code or new password field. `{max}` in `passwordTooShort` becomes the
|
|
6
|
+
* API's least number of characters. The flow shows the API's own message for a code that is not
|
|
7
|
+
* here, such as `weakPassword` from a password rule the app adds.
|
|
8
|
+
*/
|
|
9
|
+
export interface ResetPasswordValidationStrings {
|
|
10
|
+
required: string;
|
|
11
|
+
invalidEmail: string;
|
|
12
|
+
passwordTooShort: string;
|
|
13
|
+
passwordRequiresDigit: string;
|
|
14
|
+
passwordRequiresLower: string;
|
|
15
|
+
passwordRequiresUpper: string;
|
|
16
|
+
passwordRequiresNonAlphanumeric: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The flow's text for each code of SjolystInnovation.AppKit.Auth's reset endpoints that is not
|
|
21
|
+
* about one field. The flow shows the API's own message for a code that is not here.
|
|
22
|
+
*/
|
|
23
|
+
export interface ResetPasswordErrorStrings {
|
|
24
|
+
/** The code is wrong or expired, or the email has no active account. */
|
|
25
|
+
invalidResetCode: string;
|
|
26
|
+
/** Five wrong codes in a row. The user has to ask for a new code. */
|
|
27
|
+
tooManyResetAttempts: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
3
30
|
/**
|
|
4
31
|
* The reset flow's text. Pass the app's own language. The defaults are English. A text that is null
|
|
5
32
|
* is left out of the page.
|
|
@@ -29,6 +56,10 @@ export interface ResetPasswordFlowStrings {
|
|
|
29
56
|
/** The text shown for a password the rules refuse. Null lists each rule it breaks. */
|
|
30
57
|
passwordInvalid: string | null;
|
|
31
58
|
somethingWentWrong: string;
|
|
59
|
+
/** The field error texts, by code. Any code left out keeps its default. */
|
|
60
|
+
validation: ResetPasswordValidationStrings;
|
|
61
|
+
/** The texts for the other refusals, by code. Any code left out keeps its default. */
|
|
62
|
+
errors: ResetPasswordErrorStrings;
|
|
32
63
|
}
|
|
33
64
|
|
|
34
65
|
export const defaultResetPasswordFlowStrings: ResetPasswordFlowStrings = {
|
|
@@ -49,4 +80,17 @@ export const defaultResetPasswordFlowStrings: ResetPasswordFlowStrings = {
|
|
|
49
80
|
done: null,
|
|
50
81
|
passwordInvalid: null,
|
|
51
82
|
somethingWentWrong: "Something went wrong",
|
|
83
|
+
validation: {
|
|
84
|
+
required: "Required.",
|
|
85
|
+
invalidEmail: "Not a valid email address.",
|
|
86
|
+
passwordTooShort: "Passwords must be at least {max} characters.",
|
|
87
|
+
passwordRequiresDigit: "Passwords must have at least one digit ('0'-'9').",
|
|
88
|
+
passwordRequiresLower: "Passwords must have at least one lowercase ('a'-'z').",
|
|
89
|
+
passwordRequiresUpper: "Passwords must have at least one uppercase ('A'-'Z').",
|
|
90
|
+
passwordRequiresNonAlphanumeric: "Passwords must have at least one non alphanumeric character.",
|
|
91
|
+
},
|
|
92
|
+
errors: {
|
|
93
|
+
invalidResetCode: "Invalid or expired reset code.",
|
|
94
|
+
tooManyResetAttempts: "Too many attempts. Request a new reset code.",
|
|
95
|
+
},
|
|
52
96
|
};
|