@salesforce/ui-bundle-template-app-react-sample-b2x 2.2.1 → 3.1.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 (81) hide show
  1. package/dist/CHANGELOG.md +17 -0
  2. package/dist/README.md +77 -27
  3. package/dist/force-app/main/default/applications/PropertyManagement.app-meta.xml +33 -0
  4. package/dist/force-app/main/default/data/Application__c.json +10 -10
  5. package/dist/force-app/main/default/data/data-plan.json +0 -6
  6. package/dist/force-app/main/default/objects/Application__c/fields/User__c.field-meta.xml +2 -2
  7. package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +1 -1
  8. package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +76 -0
  9. package/dist/force-app/main/default/permissionsets/Property_Rental_Guest_User_Access.permissionset-meta.xml +217 -0
  10. package/dist/force-app/main/default/permissionsets/Tenant_Maintenance_Access.permissionset-meta.xml +1 -1
  11. package/dist/force-app/main/default/profiles/Property Rental Prospect Profile.profile-meta.xml +295 -0
  12. package/dist/force-app/main/default/roles/Admin.role-meta.xml +9 -0
  13. package/dist/force-app/main/default/sharingrules/Property__c.sharingRules-meta.xml +17 -0
  14. package/dist/force-app/main/default/tabs/Agent__c.tab-meta.xml +5 -0
  15. package/dist/force-app/main/default/tabs/Application__c.tab-meta.xml +5 -0
  16. package/dist/force-app/main/default/tabs/KPI_Snapshot__c.tab-meta.xml +5 -0
  17. package/dist/force-app/main/default/tabs/Lease__c.tab-meta.xml +5 -0
  18. package/dist/force-app/main/default/tabs/Maintenance_Request__c.tab-meta.xml +5 -0
  19. package/dist/force-app/main/default/tabs/Maintenance_Worker__c.tab-meta.xml +5 -0
  20. package/dist/force-app/main/default/tabs/Notification__c.tab-meta.xml +5 -0
  21. package/dist/force-app/main/default/tabs/Payment__c.tab-meta.xml +5 -0
  22. package/dist/force-app/main/default/tabs/Property_Cost__c.tab-meta.xml +5 -0
  23. package/dist/force-app/main/default/tabs/Property_Feature__c.tab-meta.xml +5 -0
  24. package/dist/force-app/main/default/tabs/Property_Image__c.tab-meta.xml +5 -0
  25. package/dist/force-app/main/default/tabs/Property_Listing__c.tab-meta.xml +5 -0
  26. package/dist/force-app/main/default/tabs/Property_Management_Company__c.tab-meta.xml +5 -0
  27. package/dist/force-app/main/default/tabs/Property_Owner__c.tab-meta.xml +5 -0
  28. package/dist/force-app/main/default/tabs/Property_Sale__c.tab-meta.xml +5 -0
  29. package/dist/force-app/main/default/tabs/Property__c.tab-meta.xml +5 -0
  30. package/dist/force-app/main/default/tabs/Tenant__c.tab-meta.xml +5 -0
  31. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/applications/applicationApi.ts +5 -6
  32. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/graphql-operations-types.ts +14751 -2937
  33. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/maintenanceRequests/maintenanceRequestApi.ts +4 -0
  34. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/query/tenantProperties.graphql +24 -0
  35. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/tenants/tenantApi.ts +23 -0
  36. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/alerts/status-alert.tsx +11 -8
  37. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/input.tsx +1 -1
  38. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/context/TenantAccessContext.tsx +24 -12
  39. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/api/userProfileApi.ts +2 -1
  40. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/authenticationConfig.ts +9 -9
  41. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/context/AuthContext.tsx +21 -4
  42. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/forms/auth-form.tsx +15 -1
  43. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/hooks/form.tsx +1 -1
  44. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/privateRouteLayout.tsx +2 -11
  45. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ChangePassword.tsx +20 -4
  46. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ForgotPassword.tsx +19 -4
  47. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Login.tsx +19 -4
  48. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Profile.tsx +80 -43
  49. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Register.tsx +15 -4
  50. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ResetPassword.tsx +19 -4
  51. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/utils/helpers.ts +15 -52
  52. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +2 -4
  53. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +7 -15
  54. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +9 -5
  55. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/SearchFilter.tsx +5 -3
  56. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/TextFilter.tsx +5 -3
  57. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useAsyncData.ts +11 -4
  58. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useAsyncData.ts +67 -0
  59. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useGeocode.ts +5 -9
  60. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useMaintenanceRequests.ts +3 -11
  61. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/usePropertyDetail.ts +6 -17
  62. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/usePropertyMapMarkers.ts +43 -34
  63. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useWeather.ts +2 -3
  64. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Application.tsx +45 -44
  65. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Contact.tsx +5 -9
  66. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Home.tsx +2 -3
  67. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Maintenance.tsx +43 -15
  68. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx +21 -19
  69. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/routes.tsx +19 -25
  70. package/dist/package-lock.json +2 -2
  71. package/dist/package.json +1 -1
  72. package/dist/scripts/org-setup.config.json +18 -3
  73. package/dist/scripts/org-setup.mjs +528 -44
  74. package/package.json +1 -1
  75. package/dist/force-app/main/default/data/Contact.json +0 -44
  76. package/dist/force-app/main/default/scripts/org-setup.config.json +0 -9
  77. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/query/tenantAccess.graphql +0 -13
  78. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/tenantApi.ts +0 -12
  79. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layout/card-skeleton.tsx +0 -38
  80. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/authenticationRouteLayout.tsx +0 -21
  81. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useCachedAsyncData.ts +0 -188
@@ -43,6 +43,8 @@ export interface CreateMaintenanceRequestInput {
43
43
  Priority__c?: string;
44
44
  Status__c?: string;
45
45
  Scheduled__c?: string | null;
46
+ Property__c?: string | null;
47
+ User__c?: string | null;
46
48
  }
47
49
 
48
50
  function getRecordIdFromResponse(result: Record<string, unknown>): string {
@@ -66,6 +68,8 @@ export async function createMaintenanceRequest(
66
68
  };
67
69
  if (input.Type__c?.trim()) fields.Type__c = input.Type__c.trim();
68
70
  if (input.Scheduled__c?.trim()) fields.Scheduled__c = input.Scheduled__c.trim();
71
+ if (input.Property__c?.trim()) fields.Property__c = input.Property__c.trim();
72
+ if (input.User__c?.trim()) fields.User__c = input.User__c.trim();
69
73
  const result = (await createRecord(OBJECT_API_NAME, fields)) as unknown as Record<
70
74
  string,
71
75
  unknown
@@ -0,0 +1,24 @@
1
+ query TenantProperties($userId: ID!) {
2
+ uiapi {
3
+ query {
4
+ Tenant__c(where: { User__c: { eq: $userId } }, first: 50) {
5
+ edges {
6
+ node {
7
+ Id
8
+ Property__c @optional {
9
+ value
10
+ }
11
+ Property__r @optional {
12
+ Name @optional {
13
+ value
14
+ }
15
+ Address__c @optional {
16
+ value
17
+ }
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,23 @@
1
+ import TENANT_PROPERTIES_QUERY from "../query/tenantProperties.graphql?raw";
2
+ import type {
3
+ TenantPropertiesQuery,
4
+ TenantPropertiesQueryVariables,
5
+ } from "../graphql-operations-types.js";
6
+ import { executeGraphQL } from "@/api/graphqlClient.js";
7
+
8
+ export type TenantProperty = NonNullable<
9
+ NonNullable<
10
+ NonNullable<NonNullable<TenantPropertiesQuery["uiapi"]["query"]["Tenant__c"]>["edges"]>[number]
11
+ >["node"]
12
+ > & { Property__c: { value: string } };
13
+
14
+ export async function getTenantProperties(userId: string): Promise<TenantProperty[]> {
15
+ if (!userId.trim()) return [];
16
+ const response = await executeGraphQL<TenantPropertiesQuery, TenantPropertiesQueryVariables>(
17
+ TENANT_PROPERTIES_QUERY,
18
+ { userId },
19
+ );
20
+ return (response.uiapi?.query?.Tenant__c?.edges ?? [])
21
+ .map((edge) => edge?.node)
22
+ .filter((node): node is TenantProperty => Boolean(node?.Property__c?.value));
23
+ }
@@ -1,5 +1,5 @@
1
1
  import { cva, type VariantProps } from 'class-variance-authority';
2
- import { AlertCircleIcon, CheckCircle2Icon } from 'lucide-react';
2
+ import { AlertCircleIcon, CheckCircle2Icon, InfoIcon } from 'lucide-react';
3
3
  import { Alert, AlertDescription } from '../../components/ui/alert';
4
4
  import { useId } from 'react';
5
5
 
@@ -8,6 +8,7 @@ const statusAlertVariants = cva('', {
8
8
  variant: {
9
9
  error: '',
10
10
  success: '',
11
+ info: 'text-blue-600 *:[svg]:text-current *:data-[slot=alert-description]:text-blue-600/90',
11
12
  },
12
13
  },
13
14
  defaultVariants: {
@@ -18,11 +19,11 @@ const statusAlertVariants = cva('', {
18
19
  interface StatusAlertProps extends VariantProps<typeof statusAlertVariants> {
19
20
  children?: React.ReactNode;
20
21
  /** Alert variant type. @default "error" */
21
- variant?: 'error' | 'success';
22
+ variant?: 'error' | 'success' | 'info';
22
23
  }
23
24
 
24
25
  /**
25
- * Status alert component for displaying error or success messages.
26
+ * Status alert component for displaying error, success, or info messages.
26
27
  * Returns null if no children are provided.
27
28
  */
28
29
  export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
@@ -31,6 +32,12 @@ export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
31
32
 
32
33
  const isError = variant === 'error';
33
34
 
35
+ const icon = {
36
+ error: <AlertCircleIcon aria-hidden="true" />,
37
+ success: <CheckCircle2Icon aria-hidden="true" />,
38
+ info: <InfoIcon aria-hidden="true" />,
39
+ }[variant];
40
+
34
41
  return (
35
42
  <Alert
36
43
  variant={isError ? 'destructive' : 'default'}
@@ -38,11 +45,7 @@ export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
38
45
  aria-describedby={descriptionId}
39
46
  role={isError ? 'alert' : 'status'}
40
47
  >
41
- {isError ? (
42
- <AlertCircleIcon aria-hidden="true" />
43
- ) : (
44
- <CheckCircle2Icon aria-hidden="true" />
45
- )}
48
+ {icon}
46
49
  <AlertDescription id={descriptionId}>{children}</AlertDescription>
47
50
  </Alert>
48
51
  );
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
8
8
  type={type}
9
9
  data-slot="input"
10
10
  className={cn(
11
- 'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
11
+ 'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground/70 placeholder:italic w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
12
12
  className
13
13
  )}
14
14
  {...props}
@@ -1,38 +1,48 @@
1
1
  import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
2
2
  import { useAuth } from "@/features/authentication/context/AuthContext";
3
- import { hasTenantAccess } from "@/api/tenantApi";
3
+ import { getTenantProperties, type TenantProperty } from "@/api/tenants/tenantApi";
4
4
 
5
5
  interface TenantAccessContextType {
6
6
  hasTenantRecord: boolean;
7
+ tenantProperties: TenantProperty[];
7
8
  loading: boolean;
8
9
  }
9
10
 
10
11
  const TenantAccessContext = createContext<TenantAccessContextType>({
11
12
  hasTenantRecord: false,
13
+ tenantProperties: [],
12
14
  loading: true,
13
15
  });
14
16
 
15
17
  export function TenantAccessProvider({ children }: { children: ReactNode }) {
16
18
  const { user } = useAuth();
17
- const [hasTenantRecord, setHasTenantRecord] = useState(false);
19
+ const [tenantProperties, setTenantProperties] = useState<TenantProperty[]>([]);
18
20
  const [loading, setLoading] = useState(true);
19
21
 
20
- useEffect(() => {
22
+ // Reset state synchronously during render when user changes
23
+ const [prevUserId, setPrevUserId] = useState(user?.id);
24
+ if (prevUserId !== user?.id) {
25
+ setPrevUserId(user?.id);
21
26
  const id = user?.id?.trim() ?? "";
22
27
  if (!id) {
23
- setHasTenantRecord(false);
24
- setLoading(false);
25
- return;
28
+ if (tenantProperties.length > 0) setTenantProperties([]);
29
+ if (loading) setLoading(false);
30
+ } else {
31
+ if (!loading) setLoading(true);
26
32
  }
33
+ }
34
+
35
+ useEffect(() => {
36
+ const id = user?.id?.trim() ?? "";
37
+ if (!id) return;
27
38
 
28
39
  let cancelled = false;
29
- setLoading(true);
30
- hasTenantAccess(id)
31
- .then((allowed) => {
32
- if (!cancelled) setHasTenantRecord(allowed);
40
+ getTenantProperties(id)
41
+ .then((props) => {
42
+ if (!cancelled) setTenantProperties(props);
33
43
  })
34
44
  .catch(() => {
35
- if (!cancelled) setHasTenantRecord(false);
45
+ if (!cancelled) setTenantProperties([]);
36
46
  })
37
47
  .finally(() => {
38
48
  if (!cancelled) setLoading(false);
@@ -43,8 +53,10 @@ export function TenantAccessProvider({ children }: { children: ReactNode }) {
43
53
  };
44
54
  }, [user?.id]);
45
55
 
56
+ const hasTenantRecord = tenantProperties.length > 0;
57
+
46
58
  return (
47
- <TenantAccessContext.Provider value={{ hasTenantRecord, loading }}>
59
+ <TenantAccessContext.Provider value={{ hasTenantRecord, tenantProperties, loading }}>
48
60
  {children}
49
61
  </TenantAccessContext.Provider>
50
62
  );
@@ -48,7 +48,8 @@ function getUserProfileMutation(fields: string): string {
48
48
 
49
49
  function throwOnGraphQLErrors(response: any): void {
50
50
  if (response?.errors?.length) {
51
- throw new Error(response.errors.map((e: any) => e.message).join("; "));
51
+ console.error("GraphQL request failed", response.errors);
52
+ throw new Error("An unexpected error occurred");
52
53
  }
53
54
  }
54
55
 
@@ -49,13 +49,13 @@ export const AUTH_REDIRECT_PARAM = "startUrl";
49
49
  * Placeholder text constants for authentication form inputs.
50
50
  */
51
51
  export const AUTH_PLACEHOLDERS = {
52
- EMAIL: "asalesforce@example.com",
53
- PASSWORD: "",
54
- PASSWORD_CREATE: "",
55
- PASSWORD_CONFIRM: "",
56
- PASSWORD_NEW: "",
57
- PASSWORD_NEW_CONFIRM: "",
58
- FIRST_NAME: "Astro",
59
- LAST_NAME: "Salesforce",
60
- USERNAME: "asalesforce",
52
+ EMAIL: "e.g. name@example.com",
53
+ PASSWORD: "Enter your password",
54
+ PASSWORD_CREATE: "Create a password",
55
+ PASSWORD_CONFIRM: "Re-enter your password",
56
+ PASSWORD_NEW: "Enter new password",
57
+ PASSWORD_NEW_CONFIRM: "Re-enter new password",
58
+ FIRST_NAME: "e.g. Alex",
59
+ LAST_NAME: "e.g. Smith",
60
+ USERNAME: "e.g. asmith",
61
61
  } as const;
@@ -35,8 +35,8 @@ export function AuthProvider({ children }: AuthProviderProps) {
35
35
  const userData = await getCurrentUser();
36
36
  setUser(userData);
37
37
  } catch (err) {
38
- const errorMessage = err instanceof Error ? err.message : "Authentication failed";
39
- setError(errorMessage);
38
+ console.error("Authentication failed", err);
39
+ setError("Authentication failed");
40
40
  setUser(null);
41
41
  } finally {
42
42
  setLoading(false);
@@ -53,8 +53,25 @@ export function AuthProvider({ children }: AuthProviderProps) {
53
53
  }, []);
54
54
 
55
55
  useEffect(() => {
56
- checkAuth();
57
- }, [checkAuth]);
56
+ let cancelled = false;
57
+ getCurrentUser()
58
+ .then((userData) => {
59
+ if (!cancelled) setUser(userData);
60
+ })
61
+ .catch((err) => {
62
+ console.error("Authentication failed", err);
63
+ if (!cancelled) {
64
+ setError("Authentication failed");
65
+ setUser(null);
66
+ }
67
+ })
68
+ .finally(() => {
69
+ if (!cancelled) setLoading(false);
70
+ });
71
+ return () => {
72
+ cancelled = true;
73
+ };
74
+ }, []);
58
75
 
59
76
  const value: AuthContextType = {
60
77
  user,
@@ -4,6 +4,7 @@ import { FooterLink } from "../footers/footer-link";
4
4
  import { SubmitButton } from "./submit-button";
5
5
  import { CardLayout } from "../../../components/layouts/card-layout";
6
6
  import { useFormContext } from "../hooks/form";
7
+ import { useAuth } from "../context/AuthContext";
7
8
  import { useId } from "react";
8
9
 
9
10
  /**
@@ -15,6 +16,8 @@ interface AuthFormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
15
16
  description: string;
16
17
  error?: React.ReactNode;
17
18
  success?: React.ReactNode;
19
+ /** Whether to show the "already logged in" alert and disable submit when authenticated. @default true */
20
+ showAlreadyLoggedIn?: boolean;
18
21
  submit: {
19
22
  text: string;
20
23
  loadingText?: string;
@@ -32,6 +35,10 @@ interface AuthFormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
32
35
  * Wraps the specific logic of Login/Register forms with a consistent visual frame (Card),
33
36
  * title, and error alert placement. Extends form element props for flexibility.
34
37
  * This ensures all auth-related pages look and behave similarly.
38
+ *
39
+ * Auth-aware behavior:
40
+ * - While auth state is loading, the submit button is disabled.
41
+ * - If the user is already authenticated, an info alert is shown and submit is disabled.
35
42
  */
36
43
  export function AuthForm({
37
44
  id: providedId,
@@ -39,18 +46,25 @@ export function AuthForm({
39
46
  description,
40
47
  error,
41
48
  success,
49
+ showAlreadyLoggedIn = true,
42
50
  children,
43
51
  submit,
44
52
  footer,
45
53
  ...props
46
54
  }: AuthFormProps) {
47
55
  const form = useFormContext();
56
+ const { isAuthenticated, loading } = useAuth();
48
57
  const generatedId = useId();
49
58
  const id = providedId ?? generatedId;
50
59
 
60
+ const showAuthAlert = showAlreadyLoggedIn && isAuthenticated;
61
+ const isSubmitDisabled = submit.disabled || showAuthAlert || loading;
62
+
51
63
  return (
52
64
  <CardLayout title={title} description={description}>
53
65
  <div className="space-y-6">
66
+ {/* [Dev Note] Auth status alert for authenticated users on public pages */}
67
+ {showAuthAlert && <StatusAlert variant="info">You are already logged in.</StatusAlert>}
54
68
  {/* [Dev Note] Global form error alert (e.g. "Invalid Credentials") */}
55
69
  {error && <StatusAlert variant="error">{error}</StatusAlert>}
56
70
  {success && <StatusAlert variant="success">{success}</StatusAlert>}
@@ -69,7 +83,7 @@ export function AuthForm({
69
83
  form={id}
70
84
  label={submit.text}
71
85
  loadingLabel={submit.loadingText}
72
- disabled={submit.disabled}
86
+ disabled={isSubmitDisabled}
73
87
  className="mt-6"
74
88
  />
75
89
  </form>
@@ -40,7 +40,7 @@ function TextField({
40
40
  const id = providedId ?? generatedId;
41
41
  const descriptionId = `${id}-description`;
42
42
  const errorId = `${id}-error`;
43
- const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
43
+ const isInvalid = field.state.meta.isBlurred && field.state.meta.errors.length > 0;
44
44
 
45
45
  // Deduplicate errors by message
46
46
  const errors = field.state.meta.errors;
@@ -1,15 +1,6 @@
1
1
  import { Navigate, Outlet, useLocation } from "react-router";
2
2
  import { useAuth } from "../context/AuthContext";
3
3
  import { AUTH_REDIRECT_PARAM, ROUTES } from "../authenticationConfig";
4
- import { CardSkeleton } from "../layout/card-skeleton";
5
-
6
- export interface PrivateRouteProps {
7
- /**
8
- * Whether to show a card skeleton placeholder while authentication is loading.
9
- * @default false
10
- */
11
- showCardSkeleton?: boolean;
12
- }
13
4
 
14
5
  /**
15
6
  * [Dev Note] Route Guard:
@@ -17,11 +8,11 @@ export interface PrivateRouteProps {
17
8
  * Otherwise, redirects to Login with a 'startUrl' parameter so the user can be
18
9
  * returned to this page after successful login.
19
10
  */
20
- export default function PrivateRoute({ showCardSkeleton = false }: PrivateRouteProps) {
11
+ export default function PrivateRoute() {
21
12
  const { isAuthenticated, loading } = useAuth();
22
13
  const location = useLocation();
23
14
 
24
- if (loading) return showCardSkeleton ? <CardSkeleton contentMaxWidth="md" /> : null;
15
+ if (loading) return null;
25
16
 
26
17
  if (!isAuthenticated) {
27
18
  const searchParams = new URLSearchParams();
@@ -7,7 +7,7 @@ import { useAppForm } from "../hooks/form";
7
7
  import { createDataSDK } from "@salesforce/platform-sdk-data";
8
8
  import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
9
9
  import { newPasswordSchema } from "../authHelpers";
10
- import { handleApiResponse, getErrorMessage } from "../utils/helpers";
10
+ import { ApiError, handleApiResponse } from "../utils/helpers";
11
11
 
12
12
  const changePasswordSchema = z
13
13
  .object({
@@ -17,7 +17,7 @@ const changePasswordSchema = z
17
17
 
18
18
  export default function ChangePassword() {
19
19
  const [success, setSuccess] = useState(false);
20
- const [submitError, setSubmitError] = useState<string | null>(null);
20
+ const [submitError, setSubmitError] = useState<React.ReactNode>(null);
21
21
 
22
22
  const form = useAppForm({
23
23
  defaultValues: { currentPassword: "", newPassword: "", confirmPassword: "" },
@@ -40,11 +40,26 @@ export default function ChangePassword() {
40
40
  Accept: "application/json",
41
41
  },
42
42
  });
43
- await handleApiResponse(response, "Password change failed");
43
+ await handleApiResponse(response);
44
44
  setSuccess(true);
45
45
  form.reset();
46
46
  } catch (err) {
47
- setSubmitError(getErrorMessage(err, "Password change failed"));
47
+ console.error("Password change failed", err);
48
+ if (err instanceof ApiError) {
49
+ setSubmitError(
50
+ err.errors.length === 1 ? (
51
+ err.errors[0]
52
+ ) : (
53
+ <ul>
54
+ {err.errors.map((e, i) => (
55
+ <li key={i}>{e}</li>
56
+ ))}
57
+ </ul>
58
+ ),
59
+ );
60
+ } else {
61
+ setSubmitError("Password change failed");
62
+ }
48
63
  }
49
64
  },
50
65
  onSubmitInvalid: () => {},
@@ -56,6 +71,7 @@ export default function ChangePassword() {
56
71
  <AuthForm
57
72
  title="Change Password"
58
73
  description="Enter your current and new password below"
74
+ showAlreadyLoggedIn={false}
59
75
  error={submitError}
60
76
  success={
61
77
  success && (
@@ -5,7 +5,7 @@ import { AuthForm } from "../forms/auth-form";
5
5
  import { useAppForm } from "../hooks/form";
6
6
  import { createDataSDK } from "@salesforce/platform-sdk-data";
7
7
  import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
8
- import { handleApiResponse, getErrorMessage } from "../utils/helpers";
8
+ import { ApiError, handleApiResponse } from "../utils/helpers";
9
9
 
10
10
  const forgotPasswordSchema = z.object({
11
11
  username: z.string().trim().toLowerCase().email("Please enter a valid username"),
@@ -13,7 +13,7 @@ const forgotPasswordSchema = z.object({
13
13
 
14
14
  export default function ForgotPassword() {
15
15
  const [success, setSuccess] = useState(false);
16
- const [submitError, setSubmitError] = useState<string | null>(null);
16
+ const [submitError, setSubmitError] = useState<React.ReactNode>(null);
17
17
 
18
18
  const form = useAppForm({
19
19
  defaultValues: { username: "" },
@@ -33,10 +33,25 @@ export default function ForgotPassword() {
33
33
  Accept: "application/json",
34
34
  },
35
35
  });
36
- await handleApiResponse(response, "Failed to send reset link");
36
+ await handleApiResponse(response);
37
37
  setSuccess(true);
38
38
  } catch (err) {
39
- setSubmitError(getErrorMessage(err, "Failed to send reset link"));
39
+ console.error("Failed to send reset link", err);
40
+ if (err instanceof ApiError) {
41
+ setSubmitError(
42
+ err.errors.length === 1 ? (
43
+ err.errors[0]
44
+ ) : (
45
+ <ul>
46
+ {err.errors.map((e, i) => (
47
+ <li key={i}>{e}</li>
48
+ ))}
49
+ </ul>
50
+ ),
51
+ );
52
+ } else {
53
+ setSubmitError("Failed to send reset link");
54
+ }
40
55
  }
41
56
  },
42
57
  onSubmitInvalid: () => {},
@@ -7,7 +7,7 @@ import { useAppForm } from "../hooks/form";
7
7
  import { createDataSDK } from "@salesforce/platform-sdk-data";
8
8
  import { ROUTES } from "../authenticationConfig";
9
9
  import { emailSchema, getStartUrl, type AuthResponse } from "../authHelpers";
10
- import { handleApiResponse, getErrorMessage } from "../utils/helpers";
10
+ import { ApiError, handleApiResponse } from "../utils/helpers";
11
11
 
12
12
  const loginSchema = z.object({
13
13
  email: emailSchema,
@@ -17,7 +17,7 @@ const loginSchema = z.object({
17
17
  export default function Login() {
18
18
  const navigate = useNavigate();
19
19
  const [searchParams] = useSearchParams();
20
- const [submitError, setSubmitError] = useState<string | null>(null);
20
+ const [submitError, setSubmitError] = useState<React.ReactNode>(null);
21
21
 
22
22
  const form = useAppForm({
23
23
  defaultValues: { email: "", password: "" },
@@ -43,7 +43,7 @@ export default function Login() {
43
43
  Accept: "application/json",
44
44
  },
45
45
  });
46
- const result = await handleApiResponse<AuthResponse>(response, "Login failed");
46
+ const result = await handleApiResponse<AuthResponse>(response);
47
47
  if (result?.redirectUrl) {
48
48
  // Hard navigate to the URL which establishes the server session cookie
49
49
  window.location.replace(result.redirectUrl);
@@ -52,7 +52,22 @@ export default function Login() {
52
52
  navigate("/", { replace: true });
53
53
  }
54
54
  } catch (err) {
55
- setSubmitError(getErrorMessage(err, "Login failed"));
55
+ console.error("Login failed", err);
56
+ if (err instanceof ApiError) {
57
+ setSubmitError(
58
+ err.errors.length === 1 ? (
59
+ err.errors[0]
60
+ ) : (
61
+ <ul>
62
+ {err.errors.map((e, i) => (
63
+ <li key={i}>{e}</li>
64
+ ))}
65
+ </ul>
66
+ ),
67
+ );
68
+ } else {
69
+ setSubmitError("Login failed");
70
+ }
56
71
  }
57
72
  },
58
73
  onSubmitInvalid: () => {},