@salesforce/ui-bundle-template-feature-react-authentication 1.117.2
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/LICENSE.txt +82 -0
- package/README.md +77 -0
- package/dist/.forceignore +15 -0
- package/dist/.husky/pre-commit +4 -0
- package/dist/.prettierignore +11 -0
- package/dist/.prettierrc +17 -0
- package/dist/AGENT.md +193 -0
- package/dist/CHANGELOG.md +2128 -0
- package/dist/README.md +28 -0
- package/dist/config/project-scratch-def.json +13 -0
- package/dist/eslint.config.js +7 -0
- package/dist/force-app/main/default/classes/UIBundleAuthUtils.cls +68 -0
- package/dist/force-app/main/default/classes/UIBundleAuthUtils.cls-meta.xml +5 -0
- package/dist/force-app/main/default/classes/UIBundleChangePassword.cls +77 -0
- package/dist/force-app/main/default/classes/UIBundleChangePassword.cls-meta.xml +5 -0
- package/dist/force-app/main/default/classes/UIBundleForgotPassword.cls +71 -0
- package/dist/force-app/main/default/classes/UIBundleForgotPassword.cls-meta.xml +5 -0
- package/dist/force-app/main/default/classes/UIBundleLogin.cls +105 -0
- package/dist/force-app/main/default/classes/UIBundleLogin.cls-meta.xml +5 -0
- package/dist/force-app/main/default/classes/UIBundleRegistration.cls +162 -0
- package/dist/force-app/main/default/classes/UIBundleRegistration.cls-meta.xml +5 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/.forceignore +15 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/.graphqlrc.yml +2 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/.prettierignore +9 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/.prettierrc +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/CHANGELOG.md +10 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/README.md +75 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/codegen.yml +95 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/components.json +18 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/e2e/app.spec.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/eslint.config.js +169 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/feature-react-authentication.uibundle-meta.xml +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/index.html +12 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/package.json +70 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/playwright.config.ts +24 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/scripts/get-graphql-schema.mjs +68 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/scripts/rewrite-e2e-assets.mjs +23 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/api/graphqlClient.ts +25 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/app.tsx +16 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/appLayout.tsx +83 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/assets/icons/book.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/assets/icons/copy.svg +4 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/assets/icons/rocket.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/assets/icons/star.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/assets/images/codey-1.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/assets/images/codey-2.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/assets/images/codey-3.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/assets/images/vibe-codey.svg +194 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/alerts/status-alert.tsx +49 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/layouts/card-layout.tsx +29 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/alert.tsx +76 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/badge.tsx +48 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/breadcrumb.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/button.tsx +67 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/calendar.tsx +232 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/card.tsx +103 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/checkbox.tsx +32 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/collapsible.tsx +33 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/datePicker.tsx +127 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/dialog.tsx +162 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/field.tsx +237 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/index.ts +84 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/input.tsx +19 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/label.tsx +22 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/pagination.tsx +132 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/popover.tsx +89 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/select.tsx +193 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/separator.tsx +26 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/skeleton.tsx +14 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/sonner.tsx +20 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/spinner.tsx +16 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/table.tsx +114 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/components/ui/tabs.tsx +88 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/api/userProfileApi.ts +95 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/authHelpers.ts +73 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/authenticationConfig.ts +61 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/context/AuthContext.tsx +95 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/footers/footer-link.tsx +36 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/forms/auth-form.tsx +81 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/forms/submit-button.tsx +49 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/hooks/form.tsx +120 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/hooks/useCountdownTimer.ts +266 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/hooks/useRetryWithBackoff.ts +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/layout/card-skeleton.tsx +38 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/layout/centered-page-layout.tsx +87 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/layouts/AuthAppLayout.tsx +12 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/layouts/authenticationRouteLayout.tsx +21 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/layouts/privateRouteLayout.tsx +44 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/pages/ChangePassword.tsx +107 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/pages/ForgotPassword.tsx +73 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/pages/Login.tsx +97 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/pages/Profile.tsx +161 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/pages/Register.tsx +133 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/pages/ResetPassword.tsx +107 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +602 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/sessionTimeout/sessionTimeService.ts +149 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/sessionTimeout/sessionTimeoutConfig.ts +77 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/features/authentication/utils/helpers.ts +121 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/lib/utils.ts +6 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/navigationMenu.tsx +80 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/pages/Home.tsx +12 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/pages/NotFound.tsx +18 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/router-utils.tsx +35 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/routes.tsx +71 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/src/styles/global.css +135 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/tsconfig.json +42 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/tsconfig.node.json +13 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/ui-bundle.json +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/vite-env.d.ts +1 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/vite.config.ts +106 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/vitest-env.d.ts +2 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/vitest.config.ts +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-authentication/vitest.setup.ts +1 -0
- package/dist/jest.config.js +6 -0
- package/dist/package-lock.json +9995 -0
- package/dist/package.json +40 -0
- package/dist/scripts/apex/hello.apex +10 -0
- package/dist/scripts/graphql-search.sh +191 -0
- package/dist/scripts/prepare-import-unique-fields.js +122 -0
- package/dist/scripts/setup-cli.mjs +563 -0
- package/dist/scripts/sf-project-setup.mjs +66 -0
- package/dist/scripts/soql/account.soql +6 -0
- package/dist/sfdx-project.json +12 -0
- package/package.json +44 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CenteredPageLayout } from "../layout/centered-page-layout";
|
|
4
|
+
import { AuthForm } from "../forms/auth-form";
|
|
5
|
+
import { useAppForm } from "../hooks/form";
|
|
6
|
+
import { createDataSDK } from "@salesforce/sdk-data";
|
|
7
|
+
import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
|
|
8
|
+
import { handleApiResponse, getErrorMessage } from "../utils/helpers";
|
|
9
|
+
|
|
10
|
+
const forgotPasswordSchema = z.object({
|
|
11
|
+
username: z.string().trim().toLowerCase().email("Please enter a valid username"),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export default function ForgotPassword() {
|
|
15
|
+
const [success, setSuccess] = useState(false);
|
|
16
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
const form = useAppForm({
|
|
19
|
+
defaultValues: { username: "" },
|
|
20
|
+
validators: { onChange: forgotPasswordSchema, onSubmit: forgotPasswordSchema },
|
|
21
|
+
onSubmit: async ({ value }) => {
|
|
22
|
+
setSubmitError(null);
|
|
23
|
+
setSuccess(false);
|
|
24
|
+
try {
|
|
25
|
+
// [Dev Note] Custom Apex Endpoint: /auth/forgot-password
|
|
26
|
+
// You must ensure this Apex class exists in your org
|
|
27
|
+
const sdk = await createDataSDK();
|
|
28
|
+
const response = await sdk.fetch!("/services/apexrest/auth/forgot-password", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
body: JSON.stringify({ username: value.username.trim() }),
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
Accept: "application/json",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
await handleApiResponse(response, "Failed to send reset link");
|
|
37
|
+
setSuccess(true);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
setSubmitError(getErrorMessage(err, "Failed to send reset link"));
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
onSubmitInvalid: () => {},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<CenteredPageLayout title={ROUTES.FORGOT_PASSWORD.TITLE}>
|
|
47
|
+
<form.AppForm>
|
|
48
|
+
<AuthForm
|
|
49
|
+
title="Forgot Password"
|
|
50
|
+
description="Enter your username and we'll send you a reset link"
|
|
51
|
+
error={submitError}
|
|
52
|
+
success={
|
|
53
|
+
success &&
|
|
54
|
+
"If that username exists in our system, you will receive a reset link shortly."
|
|
55
|
+
}
|
|
56
|
+
submit={{ text: "Send Reset Link", loadingText: "Sending…", disabled: success }}
|
|
57
|
+
footer={{ text: "Remember your password?", link: ROUTES.LOGIN.PATH, linkText: "Sign in" }}
|
|
58
|
+
>
|
|
59
|
+
<form.AppField name="username">
|
|
60
|
+
{(field) => (
|
|
61
|
+
<field.TextField
|
|
62
|
+
label="Username"
|
|
63
|
+
placeholder={AUTH_PLACEHOLDERS.USERNAME}
|
|
64
|
+
autoComplete="username"
|
|
65
|
+
disabled={success}
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
</form.AppField>
|
|
69
|
+
</AuthForm>
|
|
70
|
+
</form.AppForm>
|
|
71
|
+
</CenteredPageLayout>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useNavigate, useSearchParams, Link } from "react-router";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { CenteredPageLayout } from "../layout/centered-page-layout";
|
|
5
|
+
import { AuthForm } from "../forms/auth-form";
|
|
6
|
+
import { useAppForm } from "../hooks/form";
|
|
7
|
+
import { createDataSDK } from "@salesforce/sdk-data";
|
|
8
|
+
import { ROUTES } from "../authenticationConfig";
|
|
9
|
+
import { emailSchema, getStartUrl, type AuthResponse } from "../authHelpers";
|
|
10
|
+
import { handleApiResponse, getErrorMessage } from "../utils/helpers";
|
|
11
|
+
|
|
12
|
+
const loginSchema = z.object({
|
|
13
|
+
email: emailSchema,
|
|
14
|
+
password: z.string().min(1, "Password is required"),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default function Login() {
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
const [searchParams] = useSearchParams();
|
|
20
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
const form = useAppForm({
|
|
23
|
+
defaultValues: { email: "", password: "" },
|
|
24
|
+
validators: { onChange: loginSchema, onSubmit: loginSchema },
|
|
25
|
+
onSubmit: async ({ value }) => {
|
|
26
|
+
setSubmitError(null);
|
|
27
|
+
try {
|
|
28
|
+
// [Dev Note] Salesforce Integration:
|
|
29
|
+
// We use the Data SDK fetch to make an authenticated (or guest) call to Salesforce.
|
|
30
|
+
// "/services/apexrest/auth/login" refers to a custom Apex REST resource.
|
|
31
|
+
// You must ensure this Apex class exists in your org and handles the login logic
|
|
32
|
+
// (e.g., creating a session or returning a token).
|
|
33
|
+
const sdk = await createDataSDK();
|
|
34
|
+
const response = await sdk.fetch!("/services/apexrest/auth/login", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
email: value.email.trim().toLowerCase(),
|
|
38
|
+
password: value.password,
|
|
39
|
+
startUrl: getStartUrl(searchParams),
|
|
40
|
+
}),
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
Accept: "application/json",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
const result = await handleApiResponse<AuthResponse>(response, "Login failed");
|
|
47
|
+
if (result?.redirectUrl) {
|
|
48
|
+
// Hard navigate to the URL which establishes the server session cookie
|
|
49
|
+
window.location.replace(result.redirectUrl);
|
|
50
|
+
} else {
|
|
51
|
+
// In case redirectUrl is null, navigate to home
|
|
52
|
+
navigate("/", { replace: true });
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
setSubmitError(getErrorMessage(err, "Login failed"));
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
onSubmitInvalid: () => {},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<CenteredPageLayout title={ROUTES.LOGIN.TITLE}>
|
|
63
|
+
<form.AppForm>
|
|
64
|
+
<AuthForm
|
|
65
|
+
title="Login"
|
|
66
|
+
description="Enter your email below to login to your account"
|
|
67
|
+
error={submitError}
|
|
68
|
+
submit={{ text: "Login", loadingText: "Logging in…" }}
|
|
69
|
+
footer={{
|
|
70
|
+
text: "Don't have an account?",
|
|
71
|
+
link: ROUTES.REGISTER.PATH,
|
|
72
|
+
linkText: "Sign up",
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<form.AppField name="email">
|
|
76
|
+
{(field) => <field.EmailField label="Email" />}
|
|
77
|
+
</form.AppField>
|
|
78
|
+
<form.AppField name="password">
|
|
79
|
+
{(field) => (
|
|
80
|
+
<field.PasswordField
|
|
81
|
+
label="Password"
|
|
82
|
+
labelAction={
|
|
83
|
+
<Link
|
|
84
|
+
to={ROUTES.FORGOT_PASSWORD.PATH}
|
|
85
|
+
className="text-sm underline-offset-4 hover:underline"
|
|
86
|
+
>
|
|
87
|
+
Forgot your password?
|
|
88
|
+
</Link>
|
|
89
|
+
}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
</form.AppField>
|
|
93
|
+
</AuthForm>
|
|
94
|
+
</form.AppForm>
|
|
95
|
+
</CenteredPageLayout>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { CenteredPageLayout } from "../layout/centered-page-layout";
|
|
5
|
+
import { CardSkeleton } from "../layout/card-skeleton";
|
|
6
|
+
import { AuthForm } from "../forms/auth-form";
|
|
7
|
+
import { useAppForm } from "../hooks/form";
|
|
8
|
+
import { ROUTES } from "../authenticationConfig";
|
|
9
|
+
import { emailSchema } from "../authHelpers";
|
|
10
|
+
import { getErrorMessage } from "../utils/helpers";
|
|
11
|
+
import { useUser } from "../context/AuthContext";
|
|
12
|
+
import { fetchUserProfile, updateUserProfile } from "../api/userProfileApi";
|
|
13
|
+
|
|
14
|
+
const optionalString = z
|
|
15
|
+
.string()
|
|
16
|
+
.trim()
|
|
17
|
+
.transform((val) => (val === "" ? null : val))
|
|
18
|
+
.nullable()
|
|
19
|
+
.optional();
|
|
20
|
+
|
|
21
|
+
const profileSchema = z.object({
|
|
22
|
+
FirstName: z.string().trim().min(1, "First name is required"),
|
|
23
|
+
LastName: z.string().trim().min(1, "Last name is required"),
|
|
24
|
+
Email: emailSchema,
|
|
25
|
+
Phone: optionalString,
|
|
26
|
+
Street: optionalString,
|
|
27
|
+
City: optionalString,
|
|
28
|
+
State: optionalString,
|
|
29
|
+
PostalCode: optionalString,
|
|
30
|
+
Country: optionalString,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
type ProfileFormValues = z.infer<typeof profileSchema>;
|
|
34
|
+
|
|
35
|
+
export default function Profile() {
|
|
36
|
+
const user = useUser();
|
|
37
|
+
const [profile, setProfile] = useState<ProfileFormValues | null>(null);
|
|
38
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
39
|
+
const [success, setSuccess] = useState(false);
|
|
40
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
41
|
+
|
|
42
|
+
const form = useAppForm({
|
|
43
|
+
defaultValues: {} as ProfileFormValues,
|
|
44
|
+
validators: { onChange: profileSchema, onSubmit: profileSchema },
|
|
45
|
+
onSubmit: async ({ value }) => {
|
|
46
|
+
setSubmitError(null);
|
|
47
|
+
setSuccess(false);
|
|
48
|
+
try {
|
|
49
|
+
const updated = await updateUserProfile<Partial<ProfileFormValues>>(user.id, value);
|
|
50
|
+
// Merge with submitted values so missing fields (e.g. due to FLS) don't break the form
|
|
51
|
+
setProfile({
|
|
52
|
+
FirstName: updated.FirstName ?? value.FirstName ?? "",
|
|
53
|
+
LastName: updated.LastName ?? value.LastName ?? "",
|
|
54
|
+
Email: updated.Email ?? value.Email ?? "",
|
|
55
|
+
Phone: updated.Phone ?? value.Phone ?? null,
|
|
56
|
+
Street: updated.Street ?? value.Street ?? null,
|
|
57
|
+
City: updated.City ?? value.City ?? null,
|
|
58
|
+
State: updated.State ?? value.State ?? null,
|
|
59
|
+
PostalCode: updated.PostalCode ?? value.PostalCode ?? null,
|
|
60
|
+
Country: updated.Country ?? value.Country ?? null,
|
|
61
|
+
});
|
|
62
|
+
setSuccess(true);
|
|
63
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
setSubmitError(getErrorMessage(err, "Failed to update profile"));
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
onSubmitInvalid: () => {},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
let mounted = true;
|
|
73
|
+
fetchUserProfile<Partial<ProfileFormValues>>(user.id)
|
|
74
|
+
.then((data) => {
|
|
75
|
+
if (mounted) {
|
|
76
|
+
// Merge with defaults so missing fields (e.g. due to FLS) don't break the form
|
|
77
|
+
setProfile({
|
|
78
|
+
FirstName: data.FirstName ?? "",
|
|
79
|
+
LastName: data.LastName ?? "",
|
|
80
|
+
Email: data.Email ?? "",
|
|
81
|
+
Phone: data.Phone ?? null,
|
|
82
|
+
Street: data.Street ?? null,
|
|
83
|
+
City: data.City ?? null,
|
|
84
|
+
State: data.State ?? null,
|
|
85
|
+
PostalCode: data.PostalCode ?? null,
|
|
86
|
+
Country: data.Country ?? null,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.catch((err: any) => {
|
|
91
|
+
if (mounted) {
|
|
92
|
+
setLoadError(getErrorMessage(err, "Failed to load profile"));
|
|
93
|
+
} else {
|
|
94
|
+
console.error("Failed to load profile", err);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return () => {
|
|
98
|
+
mounted = false;
|
|
99
|
+
};
|
|
100
|
+
}, [user.id]);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (profile) {
|
|
104
|
+
const formData = profileSchema.parse(profile);
|
|
105
|
+
form.reset(formData);
|
|
106
|
+
}
|
|
107
|
+
}, [profile, form]);
|
|
108
|
+
|
|
109
|
+
if (!profile && !loadError) {
|
|
110
|
+
return <CardSkeleton contentMaxWidth="md" loadingText="Loading profile…" />;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<CenteredPageLayout contentMaxWidth="md" title={ROUTES.PROFILE.TITLE}>
|
|
115
|
+
<form.AppForm>
|
|
116
|
+
<AuthForm
|
|
117
|
+
title="Profile"
|
|
118
|
+
description="Update your account information"
|
|
119
|
+
error={loadError ?? submitError}
|
|
120
|
+
success={success && "Profile updated!"}
|
|
121
|
+
submit={{ text: "Save Changes", loadingText: "Saving…" }}
|
|
122
|
+
footer={{ link: ROUTES.CHANGE_PASSWORD.PATH, linkText: "Change password" }}
|
|
123
|
+
>
|
|
124
|
+
<form.AppField name="Email">
|
|
125
|
+
{(field) => <field.EmailField label="Email" disabled />}
|
|
126
|
+
</form.AppField>
|
|
127
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
128
|
+
<form.AppField name="FirstName">
|
|
129
|
+
{(field) => <field.TextField label="First name" autoComplete="given-name" />}
|
|
130
|
+
</form.AppField>
|
|
131
|
+
<form.AppField name="LastName">
|
|
132
|
+
{(field) => <field.TextField label="Last name" autoComplete="family-name" />}
|
|
133
|
+
</form.AppField>
|
|
134
|
+
</div>
|
|
135
|
+
<form.AppField name="Phone">
|
|
136
|
+
{(field) => <field.TextField label="Phone" type="tel" autoComplete="tel" />}
|
|
137
|
+
</form.AppField>
|
|
138
|
+
<form.AppField name="Street">
|
|
139
|
+
{(field) => <field.TextField label="Street" autoComplete="street-address" />}
|
|
140
|
+
</form.AppField>
|
|
141
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
142
|
+
<form.AppField name="City">
|
|
143
|
+
{(field) => <field.TextField label="City" autoComplete="address-level2" />}
|
|
144
|
+
</form.AppField>
|
|
145
|
+
<form.AppField name="State">
|
|
146
|
+
{(field) => <field.TextField label="State" autoComplete="address-level1" />}
|
|
147
|
+
</form.AppField>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
150
|
+
<form.AppField name="PostalCode">
|
|
151
|
+
{(field) => <field.TextField label="Postal Code" autoComplete="postal-code" />}
|
|
152
|
+
</form.AppField>
|
|
153
|
+
<form.AppField name="Country">
|
|
154
|
+
{(field) => <field.TextField label="Country" autoComplete="country-name" />}
|
|
155
|
+
</form.AppField>
|
|
156
|
+
</div>
|
|
157
|
+
</AuthForm>
|
|
158
|
+
</form.AppForm>
|
|
159
|
+
</CenteredPageLayout>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useNavigate, useSearchParams } from "react-router";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { CenteredPageLayout } from "../layout/centered-page-layout";
|
|
5
|
+
import { AuthForm } from "../forms/auth-form";
|
|
6
|
+
import { useAppForm } from "../hooks/form";
|
|
7
|
+
import { createDataSDK } from "@salesforce/sdk-data";
|
|
8
|
+
import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
|
|
9
|
+
import { emailSchema, passwordSchema, getStartUrl, type AuthResponse } from "../authHelpers";
|
|
10
|
+
import { handleApiResponse, getErrorMessage } from "../utils/helpers";
|
|
11
|
+
|
|
12
|
+
const registerSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
firstName: z.string().trim().min(1, "First name is required"),
|
|
15
|
+
lastName: z.string().trim().min(1, "Last name is required"),
|
|
16
|
+
email: emailSchema,
|
|
17
|
+
password: passwordSchema,
|
|
18
|
+
confirmPassword: z.string().min(1, "Please confirm your password"),
|
|
19
|
+
startUrl: z.string(),
|
|
20
|
+
})
|
|
21
|
+
.refine((data) => data.password === data.confirmPassword, {
|
|
22
|
+
message: "Passwords do not match",
|
|
23
|
+
path: ["confirmPassword"],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export default function Register() {
|
|
27
|
+
const navigate = useNavigate();
|
|
28
|
+
const [searchParams] = useSearchParams();
|
|
29
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
const form = useAppForm({
|
|
32
|
+
defaultValues: {
|
|
33
|
+
firstName: "",
|
|
34
|
+
lastName: "",
|
|
35
|
+
email: "",
|
|
36
|
+
password: "",
|
|
37
|
+
confirmPassword: "",
|
|
38
|
+
startUrl: getStartUrl(searchParams) || "",
|
|
39
|
+
},
|
|
40
|
+
validators: { onChange: registerSchema, onSubmit: registerSchema },
|
|
41
|
+
onSubmit: async ({ value: formFieldValues }) => {
|
|
42
|
+
setSubmitError(null);
|
|
43
|
+
try {
|
|
44
|
+
// [Dev Note] Salesforce Integration:
|
|
45
|
+
// We use the Data SDK fetch to make an authenticated (or guest) call to Salesforce.
|
|
46
|
+
// "/services/apexrest/auth/register" refers to a custom Apex Class exposed as a REST resource.
|
|
47
|
+
// You must ensure this Apex class exists in your org and handles registration
|
|
48
|
+
// (e.g., duplicate checks and user creation such as Site.createExternalUser).
|
|
49
|
+
const { confirmPassword: _confirmPassword, ...request } = formFieldValues;
|
|
50
|
+
const sdk = await createDataSDK();
|
|
51
|
+
const response = await sdk.fetch!("/services/apexrest/auth/register", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
body: JSON.stringify({ request }),
|
|
54
|
+
headers: {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
Accept: "application/json",
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const result = await handleApiResponse<AuthResponse>(response, "Registration failed");
|
|
60
|
+
if (result?.redirectUrl) {
|
|
61
|
+
// Hard navigate to the URL which logs the new user in
|
|
62
|
+
window.location.replace(result.redirectUrl);
|
|
63
|
+
} else {
|
|
64
|
+
// In case redirectUrl is null, redirect to the login page
|
|
65
|
+
navigate(ROUTES.LOGIN.PATH, { replace: true });
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
setSubmitError(getErrorMessage(err, "Registration failed"));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
onSubmitInvalid: () => {},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<CenteredPageLayout title={ROUTES.REGISTER.TITLE}>
|
|
76
|
+
<form.AppForm>
|
|
77
|
+
<AuthForm
|
|
78
|
+
title="Sign Up"
|
|
79
|
+
description="Enter your information to create an account"
|
|
80
|
+
error={submitError}
|
|
81
|
+
submit={{ text: "Create an account", loadingText: "Creating account…" }}
|
|
82
|
+
footer={{
|
|
83
|
+
text: "Already have an account?",
|
|
84
|
+
link: ROUTES.LOGIN.PATH,
|
|
85
|
+
linkText: "Sign in",
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
89
|
+
<form.AppField name="firstName">
|
|
90
|
+
{(field) => (
|
|
91
|
+
<field.TextField
|
|
92
|
+
label="First name"
|
|
93
|
+
placeholder={AUTH_PLACEHOLDERS.FIRST_NAME}
|
|
94
|
+
autoComplete="given-name"
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
</form.AppField>
|
|
98
|
+
<form.AppField name="lastName">
|
|
99
|
+
{(field) => (
|
|
100
|
+
<field.TextField
|
|
101
|
+
label="Last name"
|
|
102
|
+
placeholder={AUTH_PLACEHOLDERS.LAST_NAME}
|
|
103
|
+
autoComplete="family-name"
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
</form.AppField>
|
|
107
|
+
</div>
|
|
108
|
+
<form.AppField name="email">
|
|
109
|
+
{(field) => <field.EmailField label="Email" />}
|
|
110
|
+
</form.AppField>
|
|
111
|
+
<form.AppField name="password">
|
|
112
|
+
{(field) => (
|
|
113
|
+
<field.PasswordField
|
|
114
|
+
label="Password"
|
|
115
|
+
placeholder={AUTH_PLACEHOLDERS.PASSWORD_CREATE}
|
|
116
|
+
autoComplete="new-password"
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
</form.AppField>
|
|
120
|
+
<form.AppField name="confirmPassword">
|
|
121
|
+
{(field) => (
|
|
122
|
+
<field.PasswordField
|
|
123
|
+
label="Confirm Password"
|
|
124
|
+
placeholder={AUTH_PLACEHOLDERS.PASSWORD_CONFIRM}
|
|
125
|
+
autoComplete="new-password"
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
</form.AppField>
|
|
129
|
+
</AuthForm>
|
|
130
|
+
</form.AppForm>
|
|
131
|
+
</CenteredPageLayout>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Link, useSearchParams } from "react-router";
|
|
3
|
+
import { CardLayout } from "../../../components/layouts/card-layout";
|
|
4
|
+
import { CenteredPageLayout } from "../layout/centered-page-layout";
|
|
5
|
+
import { AuthForm } from "../forms/auth-form";
|
|
6
|
+
import { StatusAlert } from "../../../components/alerts/status-alert";
|
|
7
|
+
import { useAppForm } from "../hooks/form";
|
|
8
|
+
import { createDataSDK } from "@salesforce/sdk-data";
|
|
9
|
+
import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
|
|
10
|
+
import { newPasswordSchema } from "../authHelpers";
|
|
11
|
+
import { handleApiResponse, getErrorMessage } from "../utils/helpers";
|
|
12
|
+
|
|
13
|
+
export default function ResetPassword() {
|
|
14
|
+
const [searchParams] = useSearchParams();
|
|
15
|
+
const token = searchParams.get("token");
|
|
16
|
+
const [success, setSuccess] = useState(false);
|
|
17
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
18
|
+
|
|
19
|
+
const form = useAppForm({
|
|
20
|
+
defaultValues: { newPassword: "", confirmPassword: "" },
|
|
21
|
+
validators: { onChange: newPasswordSchema, onSubmit: newPasswordSchema },
|
|
22
|
+
onSubmit: async ({ value }) => {
|
|
23
|
+
setSubmitError(null);
|
|
24
|
+
setSuccess(false);
|
|
25
|
+
try {
|
|
26
|
+
// [Dev Note] Custom Apex Endpoint: /auth/reset-password
|
|
27
|
+
// You must ensure this Apex class exists in your org
|
|
28
|
+
const sdk = await createDataSDK();
|
|
29
|
+
const response = await sdk.fetch!("/services/apexrest/auth/reset-password", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
body: JSON.stringify({ token, newPassword: value.newPassword }),
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
Accept: "application/json",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
await handleApiResponse(response, "Password reset failed");
|
|
38
|
+
setSuccess(true);
|
|
39
|
+
// Scroll to top of page after successful submission so user sees it
|
|
40
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setSubmitError(getErrorMessage(err, "Password reset failed"));
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
onSubmitInvalid: () => {},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!token) {
|
|
49
|
+
return (
|
|
50
|
+
<CenteredPageLayout title={ROUTES.RESET_PASSWORD.TITLE}>
|
|
51
|
+
<CardLayout title="Reset Password">
|
|
52
|
+
<StatusAlert>
|
|
53
|
+
Reset token is invalid or expired.{" "}
|
|
54
|
+
<Link to={ROUTES.FORGOT_PASSWORD.PATH} className="underline underline-offset-4">
|
|
55
|
+
Request a new password reset link.
|
|
56
|
+
</Link>
|
|
57
|
+
</StatusAlert>
|
|
58
|
+
</CardLayout>
|
|
59
|
+
</CenteredPageLayout>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<CenteredPageLayout title={ROUTES.RESET_PASSWORD.TITLE}>
|
|
65
|
+
<form.AppForm>
|
|
66
|
+
<AuthForm
|
|
67
|
+
title="Reset Password"
|
|
68
|
+
description="Enter your new password below"
|
|
69
|
+
error={submitError}
|
|
70
|
+
success={
|
|
71
|
+
success && (
|
|
72
|
+
<>
|
|
73
|
+
Password reset successfully!{" "}
|
|
74
|
+
<Link to={ROUTES.LOGIN.PATH} className="underline">
|
|
75
|
+
Sign in
|
|
76
|
+
</Link>
|
|
77
|
+
</>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
submit={{ text: "Reset Password", loadingText: "Resetting…" }}
|
|
81
|
+
footer={{ text: "Remember your password?", link: ROUTES.LOGIN.PATH, linkText: "Sign in" }}
|
|
82
|
+
>
|
|
83
|
+
<form.AppField name="newPassword">
|
|
84
|
+
{(field) => (
|
|
85
|
+
<field.PasswordField
|
|
86
|
+
label="New Password"
|
|
87
|
+
placeholder={AUTH_PLACEHOLDERS.PASSWORD_NEW}
|
|
88
|
+
autoComplete="new-password"
|
|
89
|
+
disabled={success}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
</form.AppField>
|
|
93
|
+
<form.AppField name="confirmPassword">
|
|
94
|
+
{(field) => (
|
|
95
|
+
<field.PasswordField
|
|
96
|
+
label="Confirm Password"
|
|
97
|
+
placeholder={AUTH_PLACEHOLDERS.PASSWORD_NEW_CONFIRM}
|
|
98
|
+
autoComplete="new-password"
|
|
99
|
+
disabled={success}
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
</form.AppField>
|
|
103
|
+
</AuthForm>
|
|
104
|
+
</form.AppForm>
|
|
105
|
+
</CenteredPageLayout>
|
|
106
|
+
);
|
|
107
|
+
}
|