@salesforce/webapp-template-app-react-template-b2e-experimental 1.43.0 → 1.43.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.
- package/dist/.a4drules/skills/install-feature/SKILL.md +0 -1
- package/dist/CHANGELOG.md +11 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/e2e/app.spec.ts +0 -7
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/package-lock.json +3589 -3223
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/package.json +2 -10
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/app.tsx +19 -13
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/appLayout.tsx +9 -1
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/AgentforceConversationClient.tsx +127 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/navigationMenu.tsx +73 -74
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/router-utils.tsx +24 -23
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/routes.tsx +0 -66
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/styles/global.css +10 -129
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/types/conversation.ts +21 -0
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/classes/WebAppAuthUtils.cls +0 -68
- package/dist/force-app/main/default/classes/WebAppAuthUtils.cls-meta.xml +0 -5
- package/dist/force-app/main/default/classes/WebAppChangePassword.cls +0 -77
- package/dist/force-app/main/default/classes/WebAppChangePassword.cls-meta.xml +0 -5
- package/dist/force-app/main/default/classes/WebAppForgotPassword.cls +0 -71
- package/dist/force-app/main/default/classes/WebAppForgotPassword.cls-meta.xml +0 -5
- package/dist/force-app/main/default/classes/WebAppLogin.cls +0 -97
- package/dist/force-app/main/default/classes/WebAppLogin.cls-meta.xml +0 -5
- package/dist/force-app/main/default/classes/WebAppRegistration.cls +0 -162
- package/dist/force-app/main/default/classes/WebAppRegistration.cls-meta.xml +0 -5
- package/dist/force-app/main/default/digitalExperienceConfigs/appreacttemplateb2e1.digitalExperienceConfig +0 -8
- package/dist/force-app/main/default/digitalExperiences/site/appreacttemplateb2e1/appreacttemplateb2e1.digitalExperience-meta.xml +0 -11
- package/dist/force-app/main/default/digitalExperiences/site/appreacttemplateb2e1/sfdc_cms__site/appreacttemplateb2e1/_meta.json +0 -5
- package/dist/force-app/main/default/digitalExperiences/site/appreacttemplateb2e1/sfdc_cms__site/appreacttemplateb2e1/content.json +0 -10
- package/dist/force-app/main/default/networks/appreacttemplateb2e.network +0 -60
- package/dist/force-app/main/default/package.xml +0 -20
- package/dist/force-app/main/default/sites/appreacttemplateb2e.site +0 -31
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/alerts/status-alert.tsx +0 -45
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/auth/authentication-route.tsx +0 -21
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/auth/private-route.tsx +0 -36
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/footers/footer-link.tsx +0 -36
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/forms/auth-form.tsx +0 -81
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/forms/submit-button.tsx +0 -49
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/layout/card-layout.tsx +0 -23
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/layout/card-skeleton.tsx +0 -38
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/layout/centered-page-layout.tsx +0 -73
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/alert.tsx +0 -65
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/button.tsx +0 -54
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/card.tsx +0 -77
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/field.tsx +0 -111
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/index.ts +0 -71
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/input.tsx +0 -19
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/label.tsx +0 -19
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/pagination.tsx +0 -99
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/select.tsx +0 -151
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/skeleton.tsx +0 -13
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/spinner.tsx +0 -26
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/table.tsx +0 -114
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/tabs.tsx +0 -115
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/context/AuthContext.tsx +0 -83
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/hooks/form.tsx +0 -116
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/lib/utils.ts +0 -6
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/About.tsx +0 -12
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/ChangePassword.tsx +0 -98
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/ForgotPassword.tsx +0 -67
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/Login.tsx +0 -89
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/Profile.tsx +0 -177
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/Register.tsx +0 -130
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/ResetPassword.tsx +0 -101
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/new.tsx +0 -10
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/utils/authenticationConfig.ts +0 -52
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/utils/helpers.ts +0 -195
package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/Profile.tsx
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
import { executeGraphQL } from "@salesforce/webapp-experimental/api";
|
|
5
|
-
|
|
6
|
-
import { CenteredPageLayout } from "../components/layout/centered-page-layout";
|
|
7
|
-
import { CardSkeleton } from "../components/layout/card-skeleton";
|
|
8
|
-
import { AuthForm } from "../components/forms/auth-form";
|
|
9
|
-
import { useAppForm } from "../hooks/form";
|
|
10
|
-
import { ROUTES } from "../utils/authenticationConfig";
|
|
11
|
-
import { emailSchema, flattenGraphQLRecord, getErrorMessage } from "../utils/helpers";
|
|
12
|
-
import { getUser } from "../context/AuthContext";
|
|
13
|
-
|
|
14
|
-
const GRAPHQL_USER_PROFILE_FIELDS = `
|
|
15
|
-
Id
|
|
16
|
-
FirstName { value }
|
|
17
|
-
LastName { value }
|
|
18
|
-
Email { value }
|
|
19
|
-
Phone { value }
|
|
20
|
-
Street { value }
|
|
21
|
-
City { value }
|
|
22
|
-
State { value }
|
|
23
|
-
PostalCode { value }
|
|
24
|
-
Country { value }`;
|
|
25
|
-
|
|
26
|
-
const QUERY_PROFILE_GRAPHQL = `
|
|
27
|
-
query GetUserProfile($userId: ID) {
|
|
28
|
-
uiapi {
|
|
29
|
-
query {
|
|
30
|
-
User(where: { Id: { eq: $userId } }) {
|
|
31
|
-
edges {
|
|
32
|
-
node {${GRAPHQL_USER_PROFILE_FIELDS}}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}`;
|
|
38
|
-
|
|
39
|
-
const MUTATE_PROFILE_GRAPHQL = `
|
|
40
|
-
mutation UpdateUserProfile($input: UserUpdateInput!) {
|
|
41
|
-
uiapi {
|
|
42
|
-
UserUpdate(input: $input) {
|
|
43
|
-
Record {${GRAPHQL_USER_PROFILE_FIELDS}}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}`;
|
|
47
|
-
|
|
48
|
-
const optionalString = z
|
|
49
|
-
.string()
|
|
50
|
-
.trim()
|
|
51
|
-
.transform((val) => (val === "" ? null : val))
|
|
52
|
-
.nullable()
|
|
53
|
-
.optional();
|
|
54
|
-
|
|
55
|
-
const profileSchema = z.object({
|
|
56
|
-
FirstName: z.string().trim().min(1, "First name is required"),
|
|
57
|
-
LastName: z.string().trim().min(1, "Last name is required"),
|
|
58
|
-
Email: emailSchema,
|
|
59
|
-
Phone: optionalString,
|
|
60
|
-
Street: optionalString,
|
|
61
|
-
City: optionalString,
|
|
62
|
-
State: optionalString,
|
|
63
|
-
PostalCode: optionalString,
|
|
64
|
-
Country: optionalString,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
type ProfileFormValues = z.infer<typeof profileSchema>;
|
|
68
|
-
|
|
69
|
-
export default function Profile() {
|
|
70
|
-
const user = getUser();
|
|
71
|
-
const [profile, setProfile] = useState<ProfileFormValues | null>(null);
|
|
72
|
-
const [loadError, setLoadError] = useState<string | null>(null);
|
|
73
|
-
const [success, setSuccess] = useState(false);
|
|
74
|
-
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
75
|
-
|
|
76
|
-
const form = useAppForm({
|
|
77
|
-
defaultValues: {} as ProfileFormValues,
|
|
78
|
-
validators: { onChange: profileSchema, onSubmit: profileSchema },
|
|
79
|
-
onSubmit: async ({ value }) => {
|
|
80
|
-
setSubmitError(null);
|
|
81
|
-
setSuccess(false);
|
|
82
|
-
try {
|
|
83
|
-
const result: any = await executeGraphQL(MUTATE_PROFILE_GRAPHQL, {
|
|
84
|
-
input: { Id: user.id, User: { ...value } },
|
|
85
|
-
});
|
|
86
|
-
setProfile(flattenGraphQLRecord(result?.uiapi?.UserUpdate?.Record));
|
|
87
|
-
|
|
88
|
-
setSuccess(true);
|
|
89
|
-
// Scroll to top of page after successful update so user sees it
|
|
90
|
-
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
91
|
-
} catch (err) {
|
|
92
|
-
setSubmitError(getErrorMessage(err, "Failed to update profile"));
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
onSubmitInvalid: () => {},
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
let mounted = true;
|
|
100
|
-
|
|
101
|
-
executeGraphQL(QUERY_PROFILE_GRAPHQL, { userId: user.id })
|
|
102
|
-
.then(
|
|
103
|
-
(result: any) =>
|
|
104
|
-
mounted && setProfile(flattenGraphQLRecord(result?.uiapi?.query?.User?.edges?.[0]?.node)),
|
|
105
|
-
)
|
|
106
|
-
.catch((err: any) => {
|
|
107
|
-
if (mounted) {
|
|
108
|
-
setLoadError(getErrorMessage(err, "Failed to load profile"));
|
|
109
|
-
} else {
|
|
110
|
-
console.error("Failed to load profile", err);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
return () => {
|
|
114
|
-
mounted = false;
|
|
115
|
-
};
|
|
116
|
-
}, [user.id]);
|
|
117
|
-
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (profile) {
|
|
120
|
-
const formData = profileSchema.parse(profile);
|
|
121
|
-
form.reset(formData);
|
|
122
|
-
}
|
|
123
|
-
}, [profile]);
|
|
124
|
-
|
|
125
|
-
if (!profile && !loadError) {
|
|
126
|
-
return <CardSkeleton contentMaxWidth="md" loadingText="Loading profile…" />;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return (
|
|
130
|
-
<CenteredPageLayout contentMaxWidth="md" title={ROUTES.PROFILE.TITLE}>
|
|
131
|
-
<form.AppForm>
|
|
132
|
-
<AuthForm
|
|
133
|
-
title="Profile"
|
|
134
|
-
description="Update your account information"
|
|
135
|
-
error={loadError ?? submitError}
|
|
136
|
-
success={success && "Profile updated!"}
|
|
137
|
-
submit={{ text: "Save Changes", loadingText: "Saving…" }}
|
|
138
|
-
footer={{ link: ROUTES.CHANGE_PASSWORD.PATH, linkText: "Change password" }}
|
|
139
|
-
>
|
|
140
|
-
<form.AppField name="Email">
|
|
141
|
-
{(field) => <field.EmailField label="Email" disabled />}
|
|
142
|
-
</form.AppField>
|
|
143
|
-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
144
|
-
<form.AppField name="FirstName">
|
|
145
|
-
{(field) => <field.TextField label="First name" autoComplete="given-name" />}
|
|
146
|
-
</form.AppField>
|
|
147
|
-
<form.AppField name="LastName">
|
|
148
|
-
{(field) => <field.TextField label="Last name" autoComplete="family-name" />}
|
|
149
|
-
</form.AppField>
|
|
150
|
-
</div>
|
|
151
|
-
<form.AppField name="Phone">
|
|
152
|
-
{(field) => <field.TextField label="Phone" type="tel" autoComplete="tel" />}
|
|
153
|
-
</form.AppField>
|
|
154
|
-
<form.AppField name="Street">
|
|
155
|
-
{(field) => <field.TextField label="Street" autoComplete="street-address" />}
|
|
156
|
-
</form.AppField>
|
|
157
|
-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
158
|
-
<form.AppField name="City">
|
|
159
|
-
{(field) => <field.TextField label="City" autoComplete="address-level2" />}
|
|
160
|
-
</form.AppField>
|
|
161
|
-
<form.AppField name="State">
|
|
162
|
-
{(field) => <field.TextField label="State" autoComplete="address-level1" />}
|
|
163
|
-
</form.AppField>
|
|
164
|
-
</div>
|
|
165
|
-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
166
|
-
<form.AppField name="PostalCode">
|
|
167
|
-
{(field) => <field.TextField label="Postal Code" autoComplete="postal-code" />}
|
|
168
|
-
</form.AppField>
|
|
169
|
-
<form.AppField name="Country">
|
|
170
|
-
{(field) => <field.TextField label="Country" autoComplete="country-name" />}
|
|
171
|
-
</form.AppField>
|
|
172
|
-
</div>
|
|
173
|
-
</AuthForm>
|
|
174
|
-
</form.AppForm>
|
|
175
|
-
</CenteredPageLayout>
|
|
176
|
-
);
|
|
177
|
-
}
|
package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/Register.tsx
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { useNavigate, useSearchParams } from "react-router";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { baseClient } from "@salesforce/webapp-experimental/api";
|
|
5
|
-
import { CenteredPageLayout } from "../components/layout/centered-page-layout";
|
|
6
|
-
import { AuthForm } from "../components/forms/auth-form";
|
|
7
|
-
import { useAppForm } from "../hooks/form";
|
|
8
|
-
import { ROUTES, AUTH_PLACEHOLDERS } from "../utils/authenticationConfig";
|
|
9
|
-
import {
|
|
10
|
-
emailSchema,
|
|
11
|
-
passwordSchema,
|
|
12
|
-
getStartUrl,
|
|
13
|
-
handleApiResponse,
|
|
14
|
-
getErrorMessage,
|
|
15
|
-
} from "../utils/helpers";
|
|
16
|
-
import type { AuthResponse } from "../utils/helpers";
|
|
17
|
-
|
|
18
|
-
const registerSchema = z
|
|
19
|
-
.object({
|
|
20
|
-
firstName: z.string().trim().min(1, "First name is required"),
|
|
21
|
-
lastName: z.string().trim().min(1, "Last name is required"),
|
|
22
|
-
email: emailSchema,
|
|
23
|
-
password: passwordSchema,
|
|
24
|
-
confirmPassword: z.string().min(1, "Please confirm your password"),
|
|
25
|
-
startUrl: z.string(),
|
|
26
|
-
})
|
|
27
|
-
.refine((data) => data.password === data.confirmPassword, {
|
|
28
|
-
message: "Passwords do not match",
|
|
29
|
-
path: ["confirmPassword"],
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
export default function Register() {
|
|
33
|
-
const navigate = useNavigate();
|
|
34
|
-
const [searchParams] = useSearchParams();
|
|
35
|
-
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
36
|
-
|
|
37
|
-
const form = useAppForm({
|
|
38
|
-
defaultValues: {
|
|
39
|
-
firstName: "",
|
|
40
|
-
lastName: "",
|
|
41
|
-
email: "",
|
|
42
|
-
password: "",
|
|
43
|
-
confirmPassword: "",
|
|
44
|
-
startUrl: getStartUrl(searchParams) || "",
|
|
45
|
-
},
|
|
46
|
-
validators: { onChange: registerSchema, onSubmit: registerSchema },
|
|
47
|
-
onSubmit: async ({ value: formFieldValues }) => {
|
|
48
|
-
setSubmitError(null);
|
|
49
|
-
try {
|
|
50
|
-
// [Dev Note] Calls the custom Apex REST endpoint for user registration.
|
|
51
|
-
// Ensure your Apex logic handles duplicate checks and user creation (e.g., Site.createExternalUser).
|
|
52
|
-
const { confirmPassword, ...request } = formFieldValues;
|
|
53
|
-
const response = await baseClient.post("/sfdcapi/services/apexrest/auth/register", {
|
|
54
|
-
request,
|
|
55
|
-
});
|
|
56
|
-
const result = await handleApiResponse<AuthResponse>(response, "Registration failed");
|
|
57
|
-
if (result?.redirectUrl) {
|
|
58
|
-
// Hard navigate to the URL which logs the new user in
|
|
59
|
-
window.location.replace(result.redirectUrl);
|
|
60
|
-
} else {
|
|
61
|
-
// In case redirectUrl is null, redirect to the login page
|
|
62
|
-
navigate(ROUTES.LOGIN.PATH, { replace: true });
|
|
63
|
-
}
|
|
64
|
-
} catch (err) {
|
|
65
|
-
setSubmitError(getErrorMessage(err, "Registration failed"));
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
onSubmitInvalid: () => {},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<CenteredPageLayout title={ROUTES.REGISTER.TITLE}>
|
|
73
|
-
<form.AppForm>
|
|
74
|
-
<AuthForm
|
|
75
|
-
title="Sign Up"
|
|
76
|
-
description="Enter your information to create an account"
|
|
77
|
-
error={submitError}
|
|
78
|
-
submit={{ text: "Create an account", loadingText: "Creating account…" }}
|
|
79
|
-
footer={{
|
|
80
|
-
text: "Already have an account?",
|
|
81
|
-
link: ROUTES.LOGIN.PATH,
|
|
82
|
-
linkText: "Sign in",
|
|
83
|
-
}}
|
|
84
|
-
>
|
|
85
|
-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 items-start">
|
|
86
|
-
<form.AppField name="firstName">
|
|
87
|
-
{(field) => (
|
|
88
|
-
<field.TextField
|
|
89
|
-
label="First name"
|
|
90
|
-
placeholder={AUTH_PLACEHOLDERS.FIRST_NAME}
|
|
91
|
-
autoComplete="given-name"
|
|
92
|
-
/>
|
|
93
|
-
)}
|
|
94
|
-
</form.AppField>
|
|
95
|
-
<form.AppField name="lastName">
|
|
96
|
-
{(field) => (
|
|
97
|
-
<field.TextField
|
|
98
|
-
label="Last name"
|
|
99
|
-
placeholder={AUTH_PLACEHOLDERS.LAST_NAME}
|
|
100
|
-
autoComplete="family-name"
|
|
101
|
-
/>
|
|
102
|
-
)}
|
|
103
|
-
</form.AppField>
|
|
104
|
-
</div>
|
|
105
|
-
<form.AppField name="email">
|
|
106
|
-
{(field) => <field.EmailField label="Email" />}
|
|
107
|
-
</form.AppField>
|
|
108
|
-
<form.AppField name="password">
|
|
109
|
-
{(field) => (
|
|
110
|
-
<field.PasswordField
|
|
111
|
-
label="Password"
|
|
112
|
-
placeholder={AUTH_PLACEHOLDERS.PASSWORD_CREATE}
|
|
113
|
-
autoComplete="new-password"
|
|
114
|
-
/>
|
|
115
|
-
)}
|
|
116
|
-
</form.AppField>
|
|
117
|
-
<form.AppField name="confirmPassword">
|
|
118
|
-
{(field) => (
|
|
119
|
-
<field.PasswordField
|
|
120
|
-
label="Confirm Password"
|
|
121
|
-
placeholder={AUTH_PLACEHOLDERS.PASSWORD_CONFIRM}
|
|
122
|
-
autoComplete="new-password"
|
|
123
|
-
/>
|
|
124
|
-
)}
|
|
125
|
-
</form.AppField>
|
|
126
|
-
</AuthForm>
|
|
127
|
-
</form.AppForm>
|
|
128
|
-
</CenteredPageLayout>
|
|
129
|
-
);
|
|
130
|
-
}
|
package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/ResetPassword.tsx
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { Link, useSearchParams } from "react-router";
|
|
3
|
-
import { baseClient } from "@salesforce/webapp-experimental/api";
|
|
4
|
-
import { CardLayout } from "../components/layout/card-layout";
|
|
5
|
-
import { CenteredPageLayout } from "../components/layout/centered-page-layout";
|
|
6
|
-
import { AuthForm } from "../components/forms/auth-form";
|
|
7
|
-
import { StatusAlert } from "../components/alerts/status-alert";
|
|
8
|
-
import { useAppForm } from "../hooks/form";
|
|
9
|
-
import { ROUTES, AUTH_PLACEHOLDERS } from "../utils/authenticationConfig";
|
|
10
|
-
import { newPasswordSchema, handleApiResponse, getErrorMessage } from "../utils/helpers";
|
|
11
|
-
|
|
12
|
-
export default function ResetPassword() {
|
|
13
|
-
const [searchParams] = useSearchParams();
|
|
14
|
-
const token = searchParams.get("token");
|
|
15
|
-
const [success, setSuccess] = useState(false);
|
|
16
|
-
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
17
|
-
|
|
18
|
-
const form = useAppForm({
|
|
19
|
-
defaultValues: { newPassword: "", confirmPassword: "" },
|
|
20
|
-
validators: { onChange: newPasswordSchema, onSubmit: newPasswordSchema },
|
|
21
|
-
onSubmit: async ({ value }) => {
|
|
22
|
-
setSubmitError(null);
|
|
23
|
-
setSuccess(false);
|
|
24
|
-
try {
|
|
25
|
-
// [Dev Note] Custom Apex Endpoint: /auth/reset-password
|
|
26
|
-
// You must ensure this Apex class exists in your org
|
|
27
|
-
const response = await baseClient.post("/services/apexrest/auth/reset-password", {
|
|
28
|
-
token,
|
|
29
|
-
newPassword: value.newPassword,
|
|
30
|
-
});
|
|
31
|
-
await handleApiResponse(response, "Password reset failed");
|
|
32
|
-
setSuccess(true);
|
|
33
|
-
// Scroll to top of page after successful submission so user sees it
|
|
34
|
-
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
35
|
-
} catch (err) {
|
|
36
|
-
setSubmitError(getErrorMessage(err, "Password reset failed"));
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
onSubmitInvalid: () => {},
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (!token) {
|
|
43
|
-
return (
|
|
44
|
-
<CenteredPageLayout title={ROUTES.RESET_PASSWORD.TITLE}>
|
|
45
|
-
<CardLayout title="Reset Password">
|
|
46
|
-
<StatusAlert>
|
|
47
|
-
Reset token is invalid or expired.{" "}
|
|
48
|
-
<Link to={ROUTES.FORGOT_PASSWORD.PATH} className="underline underline-offset-4">
|
|
49
|
-
Request a new password reset link.
|
|
50
|
-
</Link>
|
|
51
|
-
</StatusAlert>
|
|
52
|
-
</CardLayout>
|
|
53
|
-
</CenteredPageLayout>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<CenteredPageLayout title={ROUTES.RESET_PASSWORD.TITLE}>
|
|
59
|
-
<form.AppForm>
|
|
60
|
-
<AuthForm
|
|
61
|
-
title="Reset Password"
|
|
62
|
-
description="Enter your new password below"
|
|
63
|
-
error={submitError}
|
|
64
|
-
success={
|
|
65
|
-
success && (
|
|
66
|
-
<>
|
|
67
|
-
Password reset successfully!{" "}
|
|
68
|
-
<Link to={ROUTES.LOGIN.PATH} className="underline">
|
|
69
|
-
Sign in
|
|
70
|
-
</Link>
|
|
71
|
-
</>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
submit={{ text: "Reset Password", loadingText: "Resetting…" }}
|
|
75
|
-
footer={{ text: "Remember your password?", link: ROUTES.LOGIN.PATH, linkText: "Sign in" }}
|
|
76
|
-
>
|
|
77
|
-
<form.AppField name="newPassword">
|
|
78
|
-
{(field) => (
|
|
79
|
-
<field.PasswordField
|
|
80
|
-
label="New Password"
|
|
81
|
-
placeholder={AUTH_PLACEHOLDERS.PASSWORD_NEW}
|
|
82
|
-
autoComplete="new-password"
|
|
83
|
-
disabled={success}
|
|
84
|
-
/>
|
|
85
|
-
)}
|
|
86
|
-
</form.AppField>
|
|
87
|
-
<form.AppField name="confirmPassword">
|
|
88
|
-
{(field) => (
|
|
89
|
-
<field.PasswordField
|
|
90
|
-
label="Confirm Password"
|
|
91
|
-
placeholder={AUTH_PLACEHOLDERS.PASSWORD_NEW_CONFIRM}
|
|
92
|
-
autoComplete="new-password"
|
|
93
|
-
disabled={success}
|
|
94
|
-
/>
|
|
95
|
-
)}
|
|
96
|
-
</form.AppField>
|
|
97
|
-
</AuthForm>
|
|
98
|
-
</form.AppForm>
|
|
99
|
-
</CenteredPageLayout>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export default function New() {
|
|
2
|
-
return (
|
|
3
|
-
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
4
|
-
<div className="text-center">
|
|
5
|
-
<h1 className="text-4xl font-bold text-gray-900 mb-4">New</h1>
|
|
6
|
-
<p className="text-lg text-gray-600">This is the new page.</p>
|
|
7
|
-
</div>
|
|
8
|
-
</div>
|
|
9
|
-
);
|
|
10
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* [Dev Note] Centralized configuration for Auth routes.
|
|
3
|
-
* Each route contains both the path and page title.
|
|
4
|
-
* Using constants prevents typos in route paths across the application.
|
|
5
|
-
*/
|
|
6
|
-
export const ROUTES = {
|
|
7
|
-
LOGIN: {
|
|
8
|
-
PATH: "/login",
|
|
9
|
-
TITLE: "Login | MyApp",
|
|
10
|
-
},
|
|
11
|
-
REGISTER: {
|
|
12
|
-
PATH: "/register",
|
|
13
|
-
TITLE: "Create Account | MyApp",
|
|
14
|
-
},
|
|
15
|
-
FORGOT_PASSWORD: {
|
|
16
|
-
PATH: "/forgot-password",
|
|
17
|
-
TITLE: "Recover Password | MyApp",
|
|
18
|
-
},
|
|
19
|
-
RESET_PASSWORD: {
|
|
20
|
-
PATH: "/reset-password",
|
|
21
|
-
TITLE: "Reset Password | MyApp",
|
|
22
|
-
},
|
|
23
|
-
PROFILE: {
|
|
24
|
-
PATH: "/profile",
|
|
25
|
-
TITLE: "My Profile | MyApp",
|
|
26
|
-
},
|
|
27
|
-
CHANGE_PASSWORD: {
|
|
28
|
-
PATH: "/change-password",
|
|
29
|
-
TITLE: "Change Password | MyApp",
|
|
30
|
-
},
|
|
31
|
-
} as const;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* [Dev Note] Query parameter key used to store the return URL.
|
|
35
|
-
* e.g. /login?startUrl=/profile
|
|
36
|
-
*/
|
|
37
|
-
export const AUTH_REDIRECT_PARAM = "startUrl";
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Placeholder text constants for authentication form inputs.
|
|
41
|
-
*/
|
|
42
|
-
export const AUTH_PLACEHOLDERS = {
|
|
43
|
-
EMAIL: "asalesforce@example.com",
|
|
44
|
-
PASSWORD: "",
|
|
45
|
-
PASSWORD_CREATE: "",
|
|
46
|
-
PASSWORD_CONFIRM: "",
|
|
47
|
-
PASSWORD_NEW: "",
|
|
48
|
-
PASSWORD_NEW_CONFIRM: "",
|
|
49
|
-
FIRST_NAME: "Astro",
|
|
50
|
-
LAST_NAME: "Salesforce",
|
|
51
|
-
USERNAME: "asalesforce",
|
|
52
|
-
} as const;
|
package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/utils/helpers.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { AUTH_REDIRECT_PARAM } from "./authenticationConfig";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
/** Email field validation */
|
|
5
|
-
export const emailSchema = z.string().trim().email("Please enter a valid email address");
|
|
6
|
-
|
|
7
|
-
/** Password field validation (minimum 8 characters) */
|
|
8
|
-
export const passwordSchema = z.string().min(8, "Password must be at least 8 characters");
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Shared schema for new password + confirmation fields.
|
|
12
|
-
* Validates password length and matching confirmation.
|
|
13
|
-
*/
|
|
14
|
-
export const newPasswordSchema = z
|
|
15
|
-
.object({
|
|
16
|
-
newPassword: passwordSchema,
|
|
17
|
-
confirmPassword: z.string().min(1, "Please confirm your password"),
|
|
18
|
-
})
|
|
19
|
-
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
20
|
-
message: "Passwords do not match",
|
|
21
|
-
path: ["confirmPassword"],
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
* Extracts the startUrl from URLSearchParams, defaulting to '/'.
|
|
27
|
-
*
|
|
28
|
-
* SECURITY NOTE: This function strictly validates the URL to prevent
|
|
29
|
-
* Open Redirect vulnerabilities. It allows only relative paths.
|
|
30
|
-
*
|
|
31
|
-
* @param searchParams - The URLSearchParams object from useSearchParams()
|
|
32
|
-
* @returns The start URL for post-authentication redirect
|
|
33
|
-
*/
|
|
34
|
-
export function getStartUrl(searchParams: URLSearchParams): string {
|
|
35
|
-
// 1. Check for the standard redirect parameter
|
|
36
|
-
const url = searchParams.get(AUTH_REDIRECT_PARAM);
|
|
37
|
-
// 2. Security Check: Validation Logic
|
|
38
|
-
if (url && isValidRedirect(url)) {
|
|
39
|
-
return url;
|
|
40
|
-
}
|
|
41
|
-
// 3. Fallback: Default to root
|
|
42
|
-
return "/";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* [Dev Note] Security: Validates that the redirect URL is a relative path
|
|
47
|
-
* to prevent Open Redirect vulnerabilities.
|
|
48
|
-
*
|
|
49
|
-
* Security Checks:
|
|
50
|
-
* 1. Rejects protocol-relative URLs (//)
|
|
51
|
-
* 2. Rejects backslash usage which some browsers treat as slashes (/\)
|
|
52
|
-
* 3. Rejects control characters
|
|
53
|
-
*/
|
|
54
|
-
function isValidRedirect(url: string): boolean {
|
|
55
|
-
// Basic structure check
|
|
56
|
-
if (!url.startsWith("/") || url.startsWith("//")) return false;
|
|
57
|
-
// Security: Reject backslashes to prevent /\example.com bypasses
|
|
58
|
-
if (url.includes("\\")) return false;
|
|
59
|
-
// Robustness: Ensure it doesn't contain whitespace/control characters
|
|
60
|
-
if (/[^\u0021-\u00ff]/.test(url)) return false;
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* MAINTAINABILITY: Robust error extraction.
|
|
66
|
-
* Handles strings, objects, and standard Error instances.
|
|
67
|
-
*
|
|
68
|
-
* @param err - The error object (unknown type)
|
|
69
|
-
* @param fallback - Fallback message if error doesn't have a message property
|
|
70
|
-
* @returns The error message string
|
|
71
|
-
*/
|
|
72
|
-
export function getErrorMessage(err: unknown, fallback: string): string {
|
|
73
|
-
if (err instanceof Error) return err.message;
|
|
74
|
-
if (typeof err === "string") return err;
|
|
75
|
-
// Check if it's an object with a message property
|
|
76
|
-
if (typeof err === "object" && err !== null && "message" in err) {
|
|
77
|
-
return String((err as { message: unknown }).message);
|
|
78
|
-
}
|
|
79
|
-
return fallback;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* [Dev Note] Helper to parse the fetch Response.
|
|
84
|
-
* It handles the distinction between success (JSON) and failure (throwing Error).
|
|
85
|
-
*/
|
|
86
|
-
export async function handleApiResponse<T = unknown>(
|
|
87
|
-
response: Response,
|
|
88
|
-
fallbackError: string,
|
|
89
|
-
): Promise<T> {
|
|
90
|
-
// 1. Robustness: Handle 204 No Content gracefully
|
|
91
|
-
if (response.status === 204) {
|
|
92
|
-
return {} as T;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let data: any = null;
|
|
96
|
-
|
|
97
|
-
const contentType = response.headers.get("content-type");
|
|
98
|
-
if (contentType?.includes("application/json")) {
|
|
99
|
-
data = await response.json();
|
|
100
|
-
} else {
|
|
101
|
-
// [Dev Note] If Salesforce returns HTML (e.g. standard error page),
|
|
102
|
-
// we consume text to avoid parsing errors.
|
|
103
|
-
await response.text();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (!response.ok) {
|
|
107
|
-
// [Dev Note] Throwing here allows the calling component to catch and
|
|
108
|
-
// display the error via getErrorMessage()
|
|
109
|
-
throw new Error(parseApiResponseError(data, fallbackError));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return data as T;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Shared response type for authentication endpoints (login/register).
|
|
117
|
-
* Success responses contain `success: true` and `redirectUrl`.
|
|
118
|
-
* Error responses contain `errors` array.
|
|
119
|
-
*/
|
|
120
|
-
export interface AuthResponse {
|
|
121
|
-
success?: boolean;
|
|
122
|
-
redirectUrl?: string | null;
|
|
123
|
-
errors?: string[];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* UI API Record response structure.
|
|
128
|
-
*/
|
|
129
|
-
export type RecordResponse = {
|
|
130
|
-
fields: Record<
|
|
131
|
-
string,
|
|
132
|
-
{
|
|
133
|
-
value: string;
|
|
134
|
-
}
|
|
135
|
-
>;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* [Dev Note] GraphQL can return a complex nested structure.
|
|
140
|
-
* This helper flattens it to a simple object for easier form binding.
|
|
141
|
-
*
|
|
142
|
-
* @param data - Extracted payload from the GraphQL response.
|
|
143
|
-
* @param fallbackError - Fallback error message if data is null/undefined or not an object.
|
|
144
|
-
* @throws {Error} If data is not valid.
|
|
145
|
-
* @returns Flattened object with values mapped directly to the fields.
|
|
146
|
-
*/
|
|
147
|
-
export function flattenGraphQLRecord<T>(
|
|
148
|
-
data: any,
|
|
149
|
-
fallbackError: string = "An unknown error occurred",
|
|
150
|
-
): T {
|
|
151
|
-
if (!data || typeof data !== "object") {
|
|
152
|
-
throw new Error(fallbackError);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return Object.fromEntries(
|
|
156
|
-
Object.entries(data).map(([key, field]) => [
|
|
157
|
-
key,
|
|
158
|
-
field !== null && typeof field === "object" && "value" in field
|
|
159
|
-
? (field as { value: unknown }).value
|
|
160
|
-
: (field ?? null),
|
|
161
|
-
]),
|
|
162
|
-
) as T;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* [Dev Note] Salesforce APIs may return errors as an array or a single object.
|
|
167
|
-
* This helper standardizes the extraction of the error message string.
|
|
168
|
-
*
|
|
169
|
-
* @param data - The response data.
|
|
170
|
-
* @param fallbackError - Fallback error message if response doesn't have a message property
|
|
171
|
-
* @returns The error message string
|
|
172
|
-
*/
|
|
173
|
-
function parseApiResponseError(
|
|
174
|
-
data: any,
|
|
175
|
-
fallbackError: string = "An unknown error occurred",
|
|
176
|
-
): string {
|
|
177
|
-
if (data?.message) {
|
|
178
|
-
return data.message;
|
|
179
|
-
}
|
|
180
|
-
if (data?.error) {
|
|
181
|
-
return data.error;
|
|
182
|
-
}
|
|
183
|
-
if (data?.errors && Array.isArray(data.errors) && data.errors.length > 0) {
|
|
184
|
-
return data.errors.join(" ") || fallbackError;
|
|
185
|
-
}
|
|
186
|
-
if (Array.isArray(data) && data.length > 0) {
|
|
187
|
-
return (
|
|
188
|
-
data
|
|
189
|
-
.map((e) => e?.message)
|
|
190
|
-
.filter(Boolean)
|
|
191
|
-
.join(" ") || fallbackError
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
return fallbackError;
|
|
195
|
-
}
|