@salesforce/webapp-template-app-react-sample-b2x-experimental 1.79.2 → 1.80.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.
Files changed (44) hide show
  1. package/dist/CHANGELOG.md +8 -0
  2. package/dist/force-app/main/default/classes/WebAppAuthUtils.cls +68 -0
  3. package/dist/force-app/main/default/classes/WebAppAuthUtils.cls-meta.xml +5 -0
  4. package/dist/force-app/main/default/classes/WebAppChangePassword.cls +77 -0
  5. package/dist/force-app/main/default/classes/WebAppChangePassword.cls-meta.xml +5 -0
  6. package/dist/force-app/main/default/classes/WebAppForgotPassword.cls +71 -0
  7. package/dist/force-app/main/default/classes/WebAppForgotPassword.cls-meta.xml +5 -0
  8. package/dist/force-app/main/default/classes/WebAppLogin.cls +105 -0
  9. package/dist/force-app/main/default/classes/WebAppLogin.cls-meta.xml +5 -0
  10. package/dist/force-app/main/default/classes/WebAppRegistration.cls +162 -0
  11. package/dist/force-app/main/default/classes/WebAppRegistration.cls-meta.xml +5 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +3 -3
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +15 -6
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/app.tsx +4 -1
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +155 -88
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/api/userProfileApi.ts +81 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/authHelpers.ts +73 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/authenticationConfig.ts +61 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/context/AuthContext.tsx +95 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/footers/footer-link.tsx +36 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/forms/auth-form.tsx +81 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/forms/submit-button.tsx +49 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/hooks/form.tsx +120 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/hooks/useCountdownTimer.ts +266 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/hooks/useRetryWithBackoff.ts +109 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/layout/card-skeleton.tsx +38 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/layout/centered-page-layout.tsx +87 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/layouts/AuthAppLayout.tsx +12 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/layouts/authenticationRouteLayout.tsx +21 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/layouts/privateRouteLayout.tsx +36 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/ChangePassword.tsx +107 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/ForgotPassword.tsx +73 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/Login.tsx +97 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/Profile.tsx +139 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/Register.tsx +133 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/ResetPassword.tsx +107 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +616 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/sessionTimeout/sessionTimeService.ts +161 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/sessionTimeout/sessionTimeoutConfig.ts +77 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/utils/helpers.ts +121 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +201 -114
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +67 -13
  43. package/dist/package.json +1 -1
  44. package/package.json +1 -1
@@ -1,43 +1,211 @@
1
- import { useState, useCallback } from "react";
1
+ import { useState, useCallback, useEffect, type SubmitEvent } from "react";
2
2
  import { Link } from "react-router";
3
3
  import { Button } from "../components/ui/button";
4
4
  import { Input } from "../components/ui/input";
5
5
  import { Label } from "../components/ui/label";
6
6
  import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
7
- import { createContactUsLead } from "@/api/leadApi";
7
+ import { CenteredPageLayout } from "../features/authentication/layout/centered-page-layout";
8
+ import { Skeleton } from "../components/ui/skeleton";
9
+ import { createContactUsLead } from "../api/leadApi";
10
+ import { useAuth } from "../features/authentication/context/AuthContext";
11
+ import { fetchUserProfile } from "../features/authentication/api/userProfileApi";
12
+ import type { UserInfo } from "../api/leadApi";
8
13
 
9
- export default function Contact() {
14
+ function LoadingCard() {
15
+ return (
16
+ <Card>
17
+ <CardHeader>
18
+ <Skeleton className="h-4 w-2/3" />
19
+ <Skeleton className="h-4 w-1/2" />
20
+ </CardHeader>
21
+ <CardContent>
22
+ <Skeleton className="aspect-video w-full" />
23
+ </CardContent>
24
+ </Card>
25
+ );
26
+ }
27
+
28
+ function SuccessCard() {
29
+ return (
30
+ <Card>
31
+ <CardHeader>
32
+ <CardTitle className="text-xl">Message sent</CardTitle>
33
+ </CardHeader>
34
+ <CardContent className="space-y-4">
35
+ <p className="text-muted-foreground">
36
+ Thank you for reaching out. We've received your message and will get back to you soon.
37
+ </p>
38
+ <Button asChild variant="outline">
39
+ <Link to="/">Back to Home</Link>
40
+ </Button>
41
+ <Button asChild>
42
+ <Link to="/contact">Send another message</Link>
43
+ </Button>
44
+ </CardContent>
45
+ </Card>
46
+ );
47
+ }
48
+
49
+ interface ContactFormProps {
50
+ isAuthenticated: boolean;
51
+ userInfo: UserInfo | null;
52
+ onSubmit: (e: SubmitEvent<HTMLFormElement>) => void;
53
+ submitting: boolean;
54
+ submitError: string | null;
55
+ }
56
+
57
+ function ContactForm({
58
+ isAuthenticated,
59
+ userInfo,
60
+ onSubmit,
61
+ submitting,
62
+ submitError,
63
+ }: ContactFormProps) {
10
64
  const [firstName, setFirstName] = useState("");
11
65
  const [lastName, setLastName] = useState("");
12
66
  const [email, setEmail] = useState("");
13
67
  const [phone, setPhone] = useState("");
14
68
  const [subject, setSubject] = useState("");
15
69
  const [message, setMessage] = useState("");
70
+
71
+ return (
72
+ <Card>
73
+ <CardHeader>
74
+ <CardTitle className="text-lg">Send a message</CardTitle>
75
+ </CardHeader>
76
+ <CardContent>
77
+ <form onSubmit={onSubmit} className="space-y-4">
78
+ <div className="grid gap-4 sm:grid-cols-2">
79
+ <div className="space-y-2">
80
+ <Label htmlFor="contact-first">First name {!isAuthenticated && "*"}</Label>
81
+ <Input
82
+ id="contact-first"
83
+ name="contact-first"
84
+ type="text"
85
+ value={isAuthenticated ? (userInfo?.FirstName ?? "") : firstName}
86
+ onChange={(e) => setFirstName(e.target.value)}
87
+ disabled={isAuthenticated}
88
+ required={!isAuthenticated}
89
+ />
90
+ </div>
91
+ <div className="space-y-2">
92
+ <Label htmlFor="contact-last">Last name {!isAuthenticated && "*"}</Label>
93
+ <Input
94
+ id="contact-last"
95
+ name="contact-last"
96
+ type="text"
97
+ value={isAuthenticated ? (userInfo?.LastName ?? "") : lastName}
98
+ onChange={(e) => setLastName(e.target.value)}
99
+ disabled={isAuthenticated}
100
+ required={!isAuthenticated}
101
+ />
102
+ </div>
103
+ </div>
104
+ <div className="space-y-2">
105
+ <Label htmlFor="contact-email">Email {!isAuthenticated && "*"}</Label>
106
+ <Input
107
+ id="contact-email"
108
+ name="contact-email"
109
+ type="email"
110
+ value={isAuthenticated ? (userInfo?.Email ?? "") : email}
111
+ onChange={(e) => setEmail(e.target.value)}
112
+ disabled={isAuthenticated}
113
+ required={!isAuthenticated}
114
+ />
115
+ </div>
116
+ <div className="space-y-2">
117
+ <Label htmlFor="contact-phone">Phone</Label>
118
+ <Input
119
+ id="contact-phone"
120
+ name="contact-phone"
121
+ type="tel"
122
+ value={isAuthenticated ? (userInfo?.Phone ?? "") : phone}
123
+ onChange={(e) => setPhone(e.target.value)}
124
+ disabled={isAuthenticated}
125
+ />
126
+ </div>
127
+ <div className="space-y-2">
128
+ <Label htmlFor="contact-subject">Subject</Label>
129
+ <Input
130
+ id="contact-subject"
131
+ name="contact-subject"
132
+ type="text"
133
+ value={subject}
134
+ onChange={(e) => setSubject(e.target.value)}
135
+ placeholder="e.g. General inquiry, Property question"
136
+ />
137
+ </div>
138
+ <div className="space-y-2">
139
+ <Label htmlFor="contact-message">Message *</Label>
140
+ <textarea
141
+ id="contact-message"
142
+ name="contact-message"
143
+ rows={5}
144
+ value={message}
145
+ onChange={(e) => setMessage(e.target.value)}
146
+ required
147
+ className="min-h-[120px] w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
148
+ />
149
+ </div>
150
+ {submitError && <p className="text-sm text-destructive">{submitError}</p>}
151
+ <Button type="submit" disabled={submitting} className="bg-teal-600 hover:bg-teal-700">
152
+ {submitting ? "Sending…" : "Send message"}
153
+ </Button>
154
+ </form>
155
+ </CardContent>
156
+ </Card>
157
+ );
158
+ }
159
+
160
+ export default function Contact() {
161
+ const { isAuthenticated, loading, user } = useAuth();
162
+ const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
16
163
  const [submitting, setSubmitting] = useState(false);
17
164
  const [submitError, setSubmitError] = useState<string | null>(null);
18
165
  const [submitted, setSubmitted] = useState(false);
19
166
 
167
+ useEffect(() => {
168
+ if (!isAuthenticated) return;
169
+ let mounted = true;
170
+ fetchUserProfile<UserInfo>(user?.id ?? "")
171
+ .then((data) => {
172
+ if (mounted) setUserInfo(data);
173
+ })
174
+ .catch((err) => {
175
+ if (mounted) console.error("Failed to load user contact info", err);
176
+ });
177
+ return () => {
178
+ mounted = false;
179
+ };
180
+ }, [isAuthenticated, user]);
181
+
20
182
  const handleSubmit = useCallback(
21
- async (e: React.FormEvent) => {
183
+ async (e: SubmitEvent<HTMLFormElement>) => {
22
184
  e.preventDefault();
23
185
  setSubmitError(null);
24
186
  setSubmitting(true);
187
+
188
+ const form = e.currentTarget;
189
+ const formData = new FormData(form);
190
+
25
191
  try {
26
192
  await createContactUsLead({
27
- FirstName: firstName.trim(),
28
- LastName: lastName.trim(),
29
- Email: email.trim(),
30
- Phone: phone.trim() || undefined,
31
- Subject: subject.trim() || undefined,
32
- Message: message.trim(),
193
+ FirstName: isAuthenticated
194
+ ? (userInfo?.FirstName ?? "")
195
+ : (formData.get("contact-first") as string).trim(),
196
+ LastName: isAuthenticated
197
+ ? (userInfo?.LastName ?? "")
198
+ : (formData.get("contact-last") as string).trim(),
199
+ Email: isAuthenticated
200
+ ? (userInfo?.Email ?? "")
201
+ : (formData.get("contact-email") as string).trim(),
202
+ Phone: isAuthenticated
203
+ ? userInfo?.Phone
204
+ : (formData.get("contact-phone") as string)?.trim() || undefined,
205
+ Subject: (formData.get("contact-subject") as string)?.trim() || undefined,
206
+ Message: (formData.get("contact-message") as string).trim(),
33
207
  });
34
208
  setSubmitted(true);
35
- setFirstName("");
36
- setLastName("");
37
- setEmail("");
38
- setPhone("");
39
- setSubject("");
40
- setMessage("");
41
209
  } catch (err) {
42
210
  setSubmitError(
43
211
  err instanceof Error ? err.message : "Something went wrong. Please try again.",
@@ -46,113 +214,32 @@ export default function Contact() {
46
214
  setSubmitting(false);
47
215
  }
48
216
  },
49
- [firstName, lastName, email, phone, subject, message],
217
+ [isAuthenticated, userInfo],
50
218
  );
51
219
 
52
- if (submitted) {
220
+ const isLoading = loading || (isAuthenticated && !userInfo);
221
+
222
+ function renderCard() {
223
+ if (isLoading) return <LoadingCard />;
224
+ if (submitted) return <SuccessCard />;
53
225
  return (
54
- <div className="mx-auto max-w-[600px]">
55
- <Card>
56
- <CardHeader>
57
- <CardTitle className="text-xl">Message sent</CardTitle>
58
- </CardHeader>
59
- <CardContent className="space-y-4">
60
- <p className="text-muted-foreground">
61
- Thank you for reaching out. We’ve received your message and will get back to you soon.
62
- </p>
63
- <Button asChild variant="outline">
64
- <Link to="/">Back to Home</Link>
65
- </Button>
66
- <Button asChild>
67
- <Link to="/contact">Send another message</Link>
68
- </Button>
69
- </CardContent>
70
- </Card>
71
- </div>
226
+ <ContactForm
227
+ isAuthenticated={isAuthenticated}
228
+ userInfo={userInfo}
229
+ onSubmit={handleSubmit}
230
+ submitting={submitting}
231
+ submitError={submitError}
232
+ />
72
233
  );
73
234
  }
74
235
 
75
236
  return (
76
- <div className="mx-auto max-w-[600px]">
237
+ <CenteredPageLayout contentMaxWidth="md">
77
238
  <h1 className="mb-2 text-2xl font-semibold text-foreground">Contact Us</h1>
78
239
  <p className="mb-6 text-muted-foreground">
79
- Have a question or feedback? Send us a message and well respond as soon as we can.
240
+ Have a question or feedback? Send us a message and we'll respond as soon as we can.
80
241
  </p>
81
- <Card>
82
- <CardHeader>
83
- <CardTitle className="text-lg">Send a message</CardTitle>
84
- </CardHeader>
85
- <CardContent>
86
- <form onSubmit={handleSubmit} className="space-y-4">
87
- <div className="grid gap-4 sm:grid-cols-2">
88
- <div className="space-y-2">
89
- <Label htmlFor="contact-first">First name *</Label>
90
- <Input
91
- id="contact-first"
92
- type="text"
93
- value={firstName}
94
- onChange={(e) => setFirstName(e.target.value)}
95
- required
96
- />
97
- </div>
98
- <div className="space-y-2">
99
- <Label htmlFor="contact-last">Last name *</Label>
100
- <Input
101
- id="contact-last"
102
- type="text"
103
- value={lastName}
104
- onChange={(e) => setLastName(e.target.value)}
105
- required
106
- />
107
- </div>
108
- </div>
109
- <div className="space-y-2">
110
- <Label htmlFor="contact-email">Email *</Label>
111
- <Input
112
- id="contact-email"
113
- type="email"
114
- value={email}
115
- onChange={(e) => setEmail(e.target.value)}
116
- required
117
- />
118
- </div>
119
- <div className="space-y-2">
120
- <Label htmlFor="contact-phone">Phone</Label>
121
- <Input
122
- id="contact-phone"
123
- type="tel"
124
- value={phone}
125
- onChange={(e) => setPhone(e.target.value)}
126
- />
127
- </div>
128
- <div className="space-y-2">
129
- <Label htmlFor="contact-subject">Subject</Label>
130
- <Input
131
- id="contact-subject"
132
- type="text"
133
- value={subject}
134
- onChange={(e) => setSubject(e.target.value)}
135
- placeholder="e.g. General inquiry, Property question"
136
- />
137
- </div>
138
- <div className="space-y-2">
139
- <Label htmlFor="contact-message">Message *</Label>
140
- <textarea
141
- id="contact-message"
142
- rows={5}
143
- value={message}
144
- onChange={(e) => setMessage(e.target.value)}
145
- required
146
- className="min-h-[120px] w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
147
- />
148
- </div>
149
- {submitError && <p className="text-sm text-destructive">{submitError}</p>}
150
- <Button type="submit" disabled={submitting} className="bg-teal-600 hover:bg-teal-700">
151
- {submitting ? "Sending…" : "Send message"}
152
- </Button>
153
- </form>
154
- </CardContent>
155
- </Card>
156
- </div>
242
+ {renderCard()}
243
+ </CenteredPageLayout>
157
244
  );
158
245
  }
@@ -1,11 +1,20 @@
1
1
  import type { RouteObject } from 'react-router';
2
- import AppLayout from './appLayout';
3
2
  import Home from '@/pages/Home';
4
3
  import NotFound from '@/pages/NotFound';
5
4
  import GlobalSearch from "./features/global-search/pages/GlobalSearch";
6
5
  import DetailPage from "./features/global-search/pages/DetailPage";
7
6
  import { Suspense } from "react";
8
7
  import LoadingFallback from "./features/global-search/components/shared/LoadingFallback";
8
+ import Login from "./features/authentication/pages/Login";
9
+ import Register from "./features/authentication/pages/Register";
10
+ import ForgotPassword from "./features/authentication/pages/ForgotPassword";
11
+ import ResetPassword from "./features/authentication/pages/ResetPassword";
12
+ import Profile from "./features/authentication/pages/Profile";
13
+ import ChangePassword from "./features/authentication/pages/ChangePassword";
14
+ import AuthenticationRoute from "./features/authentication/layouts/authenticationRouteLayout";
15
+ import PrivateRoute from "./features/authentication/layouts/privateRouteLayout";
16
+ import { ROUTES } from "./features/authentication/authenticationConfig";
17
+ import AppLayout from "./appLayout";
9
18
  import Dashboard from "@/pages/Dashboard";
10
19
  import Maintenance from "@/pages/Maintenance";
11
20
  import PropertySearch from "@/pages/PropertySearch";
@@ -46,9 +55,44 @@ export const routes: RouteObject[] = [
46
55
  handle: { showInNavigation: false }
47
56
  },
48
57
  {
49
- path: "dashboard",
50
- element: <Dashboard />,
51
- handle: { showInNavigation: true, label: "Dashboard" }
58
+ element: <AuthenticationRoute />,
59
+ children: [
60
+ {
61
+ path: ROUTES.LOGIN.PATH,
62
+ element: <Login />,
63
+ handle: { showInNavigation: true, label: "Login", title: ROUTES.LOGIN.TITLE }
64
+ },
65
+ {
66
+ path: ROUTES.REGISTER.PATH,
67
+ element: <Register />,
68
+ handle: { showInNavigation: false, title: ROUTES.REGISTER.TITLE }
69
+ },
70
+ {
71
+ path: ROUTES.FORGOT_PASSWORD.PATH,
72
+ element: <ForgotPassword />,
73
+ handle: { showInNavigation: false, title: ROUTES.FORGOT_PASSWORD.TITLE }
74
+ },
75
+ {
76
+ path: ROUTES.RESET_PASSWORD.PATH,
77
+ element: <ResetPassword />,
78
+ handle: { showInNavigation: false, title: ROUTES.RESET_PASSWORD.TITLE }
79
+ }
80
+ ]
81
+ },
82
+ {
83
+ element: <PrivateRoute />,
84
+ children: [
85
+ {
86
+ path: ROUTES.PROFILE.PATH,
87
+ element: <Profile />,
88
+ handle: { showInNavigation: true, label: "Profile", title: ROUTES.PROFILE.TITLE }
89
+ },
90
+ {
91
+ path: ROUTES.CHANGE_PASSWORD.PATH,
92
+ element: <ChangePassword />,
93
+ handle: { showInNavigation: false, title: ROUTES.CHANGE_PASSWORD.TITLE }
94
+ }
95
+ ]
52
96
  },
53
97
  {
54
98
  path: "properties",
@@ -63,19 +107,29 @@ export const routes: RouteObject[] = [
63
107
  path: "object/Property_Listing__c/:id",
64
108
  element: <PropertyDetails />
65
109
  },
66
- {
67
- path: "maintenance",
68
- element: <Maintenance />,
69
- handle: { showInNavigation: true, label: "Maintenance" }
70
- },
71
- {
72
- path: "application",
73
- element: <Application />
74
- },
75
110
  {
76
111
  path: "contact",
77
112
  element: <Contact />,
78
113
  handle: { showInNavigation: true, label: "Contact" }
114
+ },
115
+ {
116
+ element: <PrivateRoute />,
117
+ children: [
118
+ {
119
+ path: "dashboard",
120
+ element: <Dashboard />,
121
+ handle: { showInNavigation: true, label: "Dashboard" }
122
+ },
123
+ {
124
+ path: "maintenance",
125
+ element: <Maintenance />,
126
+ handle: { showInNavigation: true, label: "Maintenance" }
127
+ },
128
+ {
129
+ path: "application",
130
+ element: <Application />
131
+ }
132
+ ]
79
133
  }
80
134
  ]
81
135
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "1.79.2",
3
+ "version": "1.80.0",
4
4
  "description": "Base SFDX project template",
5
5
  "private": true,
6
6
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-app-react-sample-b2x-experimental",
3
- "version": "1.79.2",
3
+ "version": "1.80.0",
4
4
  "description": "B2C sample app template with app shell",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",