@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
@@ -0,0 +1,161 @@
1
+ /**
2
+ * SessionTimeServlet API service
3
+ * Handles communication with the session validation endpoint
4
+ */
5
+
6
+ import { SESSION_CONFIG } from "./sessionTimeoutConfig";
7
+
8
+ /**
9
+ * Response from SessionTimeServlet API
10
+ */
11
+ export interface SessionResponse {
12
+ /** Session phase */
13
+ sp: number;
14
+ /** Seconds remaining in session */
15
+ sr: number;
16
+ }
17
+
18
+ /**
19
+ * Parse the servlet response text into SessionResponse object
20
+ * Handles CSRF protection prefix
21
+ *
22
+ * @param text - Raw response text from servlet
23
+ * @returns Parsed session response
24
+ * @throws Error if response cannot be parsed
25
+ */
26
+ function parseResponseResult(text: string): SessionResponse {
27
+ let cleanedText = text;
28
+
29
+ // Strip CSRF protection prefix if present
30
+ if (cleanedText.startsWith(SESSION_CONFIG.CSRF_TOKEN)) {
31
+ cleanedText = cleanedText.substring(SESSION_CONFIG.CSRF_TOKEN.length);
32
+ }
33
+
34
+ // Trim whitespace
35
+ cleanedText = cleanedText.trim();
36
+
37
+ try {
38
+ const parsed = JSON.parse(cleanedText) as SessionResponse;
39
+
40
+ // Validate response structure
41
+ if (typeof parsed.sp !== "number" || typeof parsed.sr !== "number") {
42
+ throw new Error("Invalid response structure: missing sp or sr properties");
43
+ }
44
+
45
+ return parsed;
46
+ } catch (error) {
47
+ console.error("[sessionTimeService] Failed to parse response:", error, "Text:", cleanedText);
48
+ throw new Error(
49
+ `Failed to parse session response: ${error instanceof Error ? error.message : "Unknown error"}`,
50
+ );
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Call SessionTimeServlet API
56
+ * Internal function used by both poll and extend functions
57
+ *
58
+ * @param basePath - Community base path (e.g., "/sfsites/c/")
59
+ * @param extend - Whether to extend the session (updateTimedOutSession param)
60
+ * @returns Session response with remaining time
61
+ * @throws Error if API call fails or security checks fail
62
+ */
63
+ async function callSessionTimeServlet(
64
+ basePath: string,
65
+ extend: boolean = false,
66
+ ): Promise<SessionResponse> {
67
+ // Build URL with cache-busting timestamp
68
+ const timestamp = Date.now();
69
+ let url = `${basePath}${SESSION_CONFIG.SERVLET_URL}?buster=${timestamp}`;
70
+
71
+ if (extend) {
72
+ url += "&updateTimedOutSession=true";
73
+ }
74
+
75
+ try {
76
+ const response = await fetch(url, {
77
+ method: "GET",
78
+ credentials: "same-origin", // Include cookies for session
79
+ cache: "no-cache",
80
+ // Security headers
81
+ headers: {
82
+ "X-Requested-With": "XMLHttpRequest", // Helps identify XHR requests
83
+ },
84
+ });
85
+
86
+ if (!response.ok) {
87
+ // Provide more context for common error codes
88
+ if (response.status === 401) {
89
+ throw new Error("Session expired or unauthorized");
90
+ } else if (response.status === 403) {
91
+ throw new Error("Access forbidden");
92
+ } else if (response.status === 404) {
93
+ throw new Error("Session endpoint not found");
94
+ } else {
95
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
96
+ }
97
+ }
98
+
99
+ // Security: Validate content type (should be text or JSON)
100
+ const contentType = response.headers.get("content-type");
101
+ if (contentType && !contentType.includes("text") && !contentType.includes("json")) {
102
+ throw new Error(`Unexpected content type: ${contentType}`);
103
+ }
104
+
105
+ const text = await response.text();
106
+ const parsed = parseResponseResult(text);
107
+
108
+ // Apply latency buffer to account for network delay
109
+ const adjustedSecondsRemaining = Math.max(0, parsed.sr - SESSION_CONFIG.LATENCY_BUFFER_SECONDS);
110
+
111
+ return {
112
+ sp: parsed.sp,
113
+ sr: adjustedSecondsRemaining,
114
+ };
115
+ } catch (error) {
116
+ // Don't log the full URL in production to avoid leaking sensitive info
117
+ console.error("[sessionTimeService] API call failed:", error);
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Poll SessionTimeServlet to check remaining session time
124
+ * Called periodically to monitor session status
125
+ *
126
+ * @param basePath - Community base path (e.g., "/sfsites/c/")
127
+ * @returns Session response with remaining time (after latency buffer adjustment)
128
+ * @throws Error if API call fails
129
+ *
130
+ * @example
131
+ * const { sr, sp } = await pollSessionTimeServlet('/sfsites/c/');
132
+ * if (sr <= 300) {
133
+ * // Less than 5 minutes remaining
134
+ * showWarning();
135
+ * }
136
+ */
137
+ export async function pollSessionTimeServlet(basePath: string): Promise<SessionResponse> {
138
+ return callSessionTimeServlet(basePath, false);
139
+ }
140
+
141
+ /**
142
+ * Extend the current session time
143
+ * Called when user clicks "Continue Working" in warning modal
144
+ *
145
+ * @param basePath - Community base path (e.g., "/sfsites/c/")
146
+ * @returns Session response with new remaining time
147
+ * @throws Error if API call fails
148
+ *
149
+ * @example
150
+ * const { sr } = await extendSessionTime('/sfsites/c/');
151
+ * console.log(`Session extended. ${sr} seconds remaining.`);
152
+ */
153
+ export async function extendSessionTime(basePath: string): Promise<SessionResponse> {
154
+ return callSessionTimeServlet(basePath, true);
155
+ }
156
+
157
+ /**
158
+ * Export parseResponseResult for testing purposes
159
+ * @internal
160
+ */
161
+ export { parseResponseResult };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Configuration constants for session timeout monitoring
3
+ */
4
+
5
+ // ============================================================================
6
+ // Retry Configuration
7
+ // ============================================================================
8
+
9
+ /** Initial delay for first retry attempt (2 seconds) */
10
+ export const INITIAL_RETRY_DELAY = 2000;
11
+
12
+ /** Maximum number of retry attempts before giving up */
13
+ export const MAX_RETRY_ATTEMPTS = 10;
14
+
15
+ /** Maximum retry delay (30 minutes) */
16
+ export const MAX_RETRY_DELAY = 30 * 60 * 1000;
17
+
18
+ // ============================================================================
19
+ // Session Storage Keys
20
+ // ============================================================================
21
+
22
+ /** sessionStorage keys used by session validator */
23
+ export const STORAGE_KEYS = {
24
+ /** Flag to show session expired message on login page */
25
+ SHOW_SESSION_MESSAGE: "lwrSessionValidator.showSessionMessage",
26
+ } as const;
27
+
28
+ // ============================================================================
29
+ // Servlet Configuration
30
+ // ============================================================================
31
+
32
+ /** SessionTimeServlet configuration */
33
+ export const SESSION_CONFIG = {
34
+ /** Relative URL to SessionTimeServlet */
35
+ SERVLET_URL: "/sfsites/c/_nc_external/system/security/session/SessionTimeServlet",
36
+
37
+ /** Latency buffer to subtract from server response (seconds) */
38
+ LATENCY_BUFFER_SECONDS: 3,
39
+
40
+ /** CSRF protection prefix in servlet responses */
41
+ CSRF_TOKEN: "while(1);\n",
42
+ } as const;
43
+
44
+ // ============================================================================
45
+ // UI Labels
46
+ // ============================================================================
47
+
48
+ /**
49
+ * UI labels for session timeout components
50
+ */
51
+ export const LABELS = {
52
+ /** Title for session warning modal */
53
+ sessionWarningTitle: "Session Timeout Warning",
54
+
55
+ /** Message text in session warning modal */
56
+ sessionWarningMessage:
57
+ "For security, we log you out if you’re inactive for too long. To continue working, click Continue before the time expires.",
58
+
59
+ /** Text for "Continue" button */
60
+ continueButton: "Continue",
61
+
62
+ /** Text for "Log Out" button */
63
+ logoutButton: "Log Out",
64
+
65
+ /** Message shown on login page after session expires */
66
+ invalidSessionMessage: "Your session has expired. Please log in again.",
67
+
68
+ /** Accessibility label for close button */
69
+ closeLabel: "Close",
70
+ } as const;
71
+
72
+ // ============================================================================
73
+ // Session Timeout Configuration
74
+ // ============================================================================
75
+
76
+ /** Session warning time in seconds (30 seconds) */
77
+ export const SESSION_WARNING_TIME = 30;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * MAINTAINABILITY: Robust error extraction.
3
+ * Handles strings, objects, and standard Error instances.
4
+ *
5
+ * @param err - The error object (unknown type)
6
+ * @param fallback - Fallback message if error doesn't have a message property
7
+ * @returns The error message string
8
+ */
9
+ export function getErrorMessage(err: unknown, fallback: string): string {
10
+ if (err instanceof Error) return err.message;
11
+ if (typeof err === "string") return err;
12
+ // Check if it's an object with a message property
13
+ if (typeof err === "object" && err !== null && "message" in err) {
14
+ return String((err as { message: unknown }).message);
15
+ }
16
+ return fallback;
17
+ }
18
+
19
+ /**
20
+ * [Dev Note] Helper to parse the fetch Response.
21
+ * It handles the distinction between success (JSON) and failure (throwing Error).
22
+ */
23
+ export async function handleApiResponse<T = unknown>(
24
+ response: Response,
25
+ fallbackError: string,
26
+ ): Promise<T> {
27
+ // 1. Robustness: Handle 204 No Content gracefully
28
+ if (response.status === 204) {
29
+ return {} as T;
30
+ }
31
+
32
+ let data: any = null;
33
+
34
+ const contentType = response.headers.get("content-type");
35
+ if (contentType?.includes("application/json")) {
36
+ data = await response.json();
37
+ } else {
38
+ // [Dev Note] If Salesforce returns HTML (e.g. standard error page),
39
+ // we consume text to avoid parsing errors.
40
+ await response.text();
41
+ }
42
+
43
+ if (!response.ok) {
44
+ // [Dev Note] Throwing here allows the calling component to catch and
45
+ // display the error via getErrorMessage()
46
+ throw new Error(parseApiResponseError(data, fallbackError));
47
+ }
48
+
49
+ return data as T;
50
+ }
51
+
52
+ /**
53
+ * UI API Record response structure.
54
+ */
55
+ export type RecordResponse = {
56
+ fields: Record<
57
+ string,
58
+ {
59
+ value: string;
60
+ }
61
+ >;
62
+ };
63
+
64
+ /**
65
+ * [Dev Note] GraphQL can return a complex nested structure.
66
+ * This helper flattens it to a simple object for easier form binding.
67
+ *
68
+ * @param data - Extracted payload from the GraphQL response.
69
+ * @param fallbackError - Fallback error message if data is null/undefined or not an object.
70
+ * @throws {Error} If data is not valid.
71
+ * @returns Flattened object with values mapped directly to the fields.
72
+ */
73
+ export function flattenGraphQLRecord<T>(
74
+ data: any,
75
+ fallbackError: string = "An unknown error occurred",
76
+ ): T {
77
+ if (!data || typeof data !== "object") {
78
+ throw new Error(fallbackError);
79
+ }
80
+
81
+ return Object.fromEntries(
82
+ Object.entries(data).map(([key, field]) => [
83
+ key,
84
+ field !== null && typeof field === "object" && "value" in field
85
+ ? (field as { value: unknown }).value
86
+ : (field ?? null),
87
+ ]),
88
+ ) as T;
89
+ }
90
+
91
+ /**
92
+ * [Dev Note] Salesforce APIs may return errors as an array or a single object.
93
+ * This helper standardizes the extraction of the error message string.
94
+ *
95
+ * @param data - The response data.
96
+ * @param fallbackError - Fallback error message if response doesn't have a message property
97
+ * @returns The error message string
98
+ */
99
+ function parseApiResponseError(
100
+ data: any,
101
+ fallbackError: string = "An unknown error occurred",
102
+ ): string {
103
+ if (data?.message) {
104
+ return data.message;
105
+ }
106
+ if (data?.error) {
107
+ return data.error;
108
+ }
109
+ if (data?.errors && Array.isArray(data.errors) && data.errors.length > 0) {
110
+ return data.errors.join(" ") || fallbackError;
111
+ }
112
+ if (Array.isArray(data) && data.length > 0) {
113
+ return (
114
+ data
115
+ .map((e) => e?.message)
116
+ .filter(Boolean)
117
+ .join(" ") || fallbackError
118
+ );
119
+ }
120
+ return fallbackError;
121
+ }