@salesforce/webapp-template-app-react-sample-b2x-experimental 1.93.0 → 1.94.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.
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.94.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.93.1...v1.94.0) (2026-03-12)
7
+
8
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
9
+
10
+
11
+
12
+
13
+
14
+ ## [1.93.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.93.0...v1.93.1) (2026-03-12)
15
+
16
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
17
+
18
+
19
+
20
+
21
+
6
22
  # [1.93.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.92.1...v1.93.0) (2026-03-11)
7
23
 
8
24
  **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
@@ -15,8 +15,8 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/sdk-data": "^1.93.0",
19
- "@salesforce/webapp-experimental": "^1.93.0",
18
+ "@salesforce/sdk-data": "^1.94.0",
19
+ "@salesforce/webapp-experimental": "^1.94.0",
20
20
  "@tailwindcss/vite": "^4.1.17",
21
21
  "@tanstack/react-form": "^1.28.4",
22
22
  "@types/leaflet": "^1.9.21",
@@ -43,7 +43,7 @@
43
43
  "@graphql-eslint/eslint-plugin": "^4.1.0",
44
44
  "@graphql-tools/utils": "^11.0.0",
45
45
  "@playwright/test": "^1.49.0",
46
- "@salesforce/vite-plugin-webapp-experimental": "^1.93.0",
46
+ "@salesforce/vite-plugin-webapp-experimental": "^1.94.0",
47
47
  "@testing-library/jest-dom": "^6.6.3",
48
48
  "@testing-library/react": "^16.1.0",
49
49
  "@testing-library/user-event": "^14.5.2",
@@ -1,22 +1,31 @@
1
1
  import { Link, useLocation } from "react-router";
2
2
  import { Home, Search, BarChart3, Wrench, Phone, type LucideIcon } from "lucide-react";
3
+ import { useAuth } from "@/features/authentication/context/AuthContext";
4
+ import { useMemo } from "react";
3
5
 
4
6
  interface NavItem {
5
7
  path: string;
6
8
  icon: LucideIcon;
7
9
  label: string;
10
+ authRequired?: boolean;
8
11
  }
9
12
 
10
13
  const navItems: NavItem[] = [
11
14
  { path: "/", icon: Home, label: "Home" },
12
- { path: "/dashboard", icon: BarChart3, label: "Dashboard" },
15
+ { path: "/dashboard", icon: BarChart3, label: "Dashboard", authRequired: true },
13
16
  { path: "/properties", icon: Search, label: "Property Search" },
14
- { path: "/maintenance/requests", icon: Wrench, label: "Maintenance Requests" },
17
+ { path: "/maintenance", icon: Wrench, label: "Maintenance Requests", authRequired: true },
15
18
  { path: "/contact", icon: Phone, label: "Contact Us" },
16
19
  ];
17
20
 
18
21
  export function NavMenu() {
19
22
  const location = useLocation();
23
+ const { isAuthenticated } = useAuth();
24
+
25
+ const visibleItems = useMemo(
26
+ () => navItems.filter((item) => !item.authRequired || isAuthenticated),
27
+ [isAuthenticated],
28
+ );
20
29
 
21
30
  const isActive = (path: string) => {
22
31
  if (path === "/") return location.pathname === "/";
@@ -28,7 +37,7 @@ export function NavMenu() {
28
37
  className="flex w-24 flex-col border-r border-gray-200 bg-white py-8"
29
38
  aria-label="Main navigation"
30
39
  >
31
- {navItems.map((item) => {
40
+ {visibleItems.map((item) => {
32
41
  const Icon = item.icon;
33
42
  const active = isActive(item.path);
34
43
  return (
@@ -1,6 +1,8 @@
1
- import { useEffect, useState } from "react";
2
- import { Search, Bell, ChevronDown, Menu } from "lucide-react";
3
- import { getUserInfo } from "@/api/userApi";
1
+ import { useState } from "react";
2
+ import { Search, Bell, ChevronDown, Menu, UserPen, LogOut, User, Loader2 } from "lucide-react";
3
+ import { Link } from "react-router";
4
+ import { useAuth } from "../features/authentication/context/AuthContext";
5
+ import { ROUTES } from "../features/authentication/authenticationConfig";
4
6
  import zenLogo from "@/assets/icons/zen-logo.svg";
5
7
 
6
8
  export interface TopBarProps {
@@ -8,26 +10,7 @@ export interface TopBarProps {
8
10
  }
9
11
 
10
12
  export function TopBar({ onMenuClick }: TopBarProps) {
11
- const [userName, setUserName] = useState<string>("User");
12
- const [showNotifications, setShowNotifications] = useState(false);
13
-
14
- useEffect(() => {
15
- const loadUserInfo = async () => {
16
- const userInfo = await getUserInfo();
17
- if (userInfo) {
18
- setUserName(userInfo.name);
19
- }
20
- };
21
- loadUserInfo();
22
- }, []);
23
-
24
- const handleNotificationClick = () => {
25
- setShowNotifications(!showNotifications);
26
- };
27
-
28
- const handleCloseNotifications = () => {
29
- setShowNotifications(false);
30
- };
13
+ const { user, isAuthenticated, loading } = useAuth();
31
14
 
32
15
  return (
33
16
  <header
@@ -43,65 +26,149 @@ export function TopBar({ onMenuClick }: TopBarProps) {
43
26
  >
44
27
  <Menu className="size-6" aria-hidden />
45
28
  </button>
46
- <div className="flex items-center gap-2">
47
- <img src={zenLogo} alt="Zenlease Logo" className="size-8" />
48
- <span className="text-xl tracking-wide">
49
- <span className="font-light">ZEN</span>
50
- <span className="font-semibold">LEASE</span>
51
- </span>
52
- </div>
29
+ <Logo />
53
30
  </div>
54
31
 
55
- <div className="flex items-center gap-4">
56
- <button
57
- type="button"
58
- className="rounded-md p-2 transition-colors hover:bg-teal-600 md:hidden"
59
- aria-label="Search"
60
- >
61
- <Search className="size-5" aria-hidden />
62
- </button>
32
+ {loading ? (
33
+ <Loader2 className="size-5 animate-spin" aria-label="Loading" />
34
+ ) : isAuthenticated ? (
35
+ <AuthenticatedControls userName={user?.name ?? "User"} />
36
+ ) : (
37
+ <LoginLink />
38
+ )}
39
+ </header>
40
+ );
41
+ }
63
42
 
64
- <div className="relative">
65
- <button
66
- type="button"
67
- onClick={handleNotificationClick}
68
- className="relative rounded-md p-2 transition-colors hover:bg-teal-600"
69
- aria-label="Notifications"
70
- >
71
- <Bell className="size-5" aria-hidden />
72
- </button>
43
+ function Logo() {
44
+ return (
45
+ <div className="flex items-center gap-2">
46
+ <img src={zenLogo} alt="Zenlease Logo" className="size-8" />
47
+ <span className="text-xl tracking-wide">
48
+ <span className="font-light">ZEN</span>
49
+ <span className="font-semibold">LEASE</span>
50
+ </span>
51
+ </div>
52
+ );
53
+ }
73
54
 
74
- {showNotifications && (
75
- <>
76
- <div className="fixed inset-0 z-40" onClick={handleCloseNotifications} aria-hidden />
77
- <div className="absolute right-0 top-full z-50 mt-2 w-80 overflow-hidden rounded-lg bg-white shadow-xl">
78
- <div className="border-b border-gray-200 p-4">
79
- <h3 className="text-sm font-semibold text-gray-900">Notifications</h3>
80
- </div>
81
- <div className="p-8 text-center">
82
- <Bell className="mx-auto mb-3 size-12 text-gray-300" aria-hidden />
83
- <p className="text-sm text-gray-500">No new notifications</p>
84
- </div>
85
- </div>
86
- </>
87
- )}
88
- </div>
55
+ function LoginLink() {
56
+ return (
57
+ <a
58
+ href={ROUTES.LOGIN.PATH}
59
+ className="flex items-center gap-2 rounded-md bg-white px-4 py-2 font-medium text-teal-700 transition-colors hover:bg-teal-50"
60
+ >
61
+ <User className="size-4" aria-hidden />
62
+ Sign Up / Sign In
63
+ </a>
64
+ );
65
+ }
89
66
 
90
- <button
91
- type="button"
92
- className="flex items-center gap-2 rounded-md px-3 py-2 transition-colors hover:bg-teal-600"
93
- aria-label="User menu"
67
+ function AuthenticatedControls({ userName }: { userName: string }) {
68
+ return (
69
+ <div className="flex items-center gap-4">
70
+ <button
71
+ type="button"
72
+ className="rounded-md p-2 transition-colors hover:bg-teal-600 md:hidden"
73
+ aria-label="Search"
74
+ >
75
+ <Search className="size-5" aria-hidden />
76
+ </button>
77
+
78
+ <NotificationBell />
79
+ <UserMenu userName={userName} />
80
+ </div>
81
+ );
82
+ }
83
+
84
+ function UserMenu({ userName }: { userName: string }) {
85
+ const { logout } = useAuth();
86
+ const [open, setOpen] = useState(false);
87
+
88
+ return (
89
+ <div className="relative">
90
+ <button
91
+ type="button"
92
+ onClick={() => setOpen((prev) => !prev)}
93
+ className="flex items-center gap-2 rounded-md px-3 py-2 transition-colors hover:bg-teal-600"
94
+ aria-label="User menu"
95
+ aria-expanded={open}
96
+ aria-haspopup="true"
97
+ >
98
+ <div
99
+ className="flex size-8 items-center justify-center rounded-full bg-teal-300 font-semibold text-teal-900"
100
+ aria-hidden
94
101
  >
102
+ {userName.charAt(0).toUpperCase()}
103
+ </div>
104
+ <span className="hidden font-medium md:inline">{userName.toUpperCase()}</span>
105
+ <ChevronDown className="hidden size-4 md:inline" aria-hidden />
106
+ </button>
107
+
108
+ {open && (
109
+ <>
110
+ <div className="fixed inset-0 z-40" onClick={() => setOpen(false)} aria-hidden />
95
111
  <div
96
- className="flex size-8 items-center justify-center rounded-full bg-teal-300 font-semibold text-teal-900"
97
- aria-hidden
112
+ className="absolute right-0 top-full z-50 mt-2 w-48 overflow-hidden rounded-lg bg-white py-1 shadow-xl"
113
+ role="menu"
98
114
  >
99
- {userName.charAt(0).toUpperCase()}
115
+ <Link
116
+ to={ROUTES.PROFILE.PATH}
117
+ onClick={() => setOpen(false)}
118
+ className="flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 transition-colors hover:bg-gray-100"
119
+ role="menuitem"
120
+ >
121
+ <UserPen className="size-4" aria-hidden />
122
+ Edit Profile
123
+ </Link>
124
+ <div className="mx-3 border-t border-gray-200" />
125
+ <button
126
+ type="button"
127
+ onClick={() => {
128
+ setOpen(false);
129
+ logout();
130
+ }}
131
+ className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-gray-700 transition-colors hover:bg-gray-100"
132
+ role="menuitem"
133
+ >
134
+ <LogOut className="size-4" aria-hidden />
135
+ Log Out
136
+ </button>
100
137
  </div>
101
- <span className="hidden font-medium md:inline">{userName.toUpperCase()}</span>
102
- <ChevronDown className="hidden size-4 md:inline" aria-hidden />
103
- </button>
104
- </div>
105
- </header>
138
+ </>
139
+ )}
140
+ </div>
141
+ );
142
+ }
143
+
144
+ function NotificationBell() {
145
+ const [open, setOpen] = useState(false);
146
+
147
+ return (
148
+ <div className="relative">
149
+ <button
150
+ type="button"
151
+ onClick={() => setOpen((prev) => !prev)}
152
+ className="relative rounded-md p-2 transition-colors hover:bg-teal-600"
153
+ aria-label="Notifications"
154
+ >
155
+ <Bell className="size-5" aria-hidden />
156
+ </button>
157
+
158
+ {open && (
159
+ <>
160
+ <div className="fixed inset-0 z-40" onClick={() => setOpen(false)} aria-hidden />
161
+ <div className="absolute right-0 top-full z-50 mt-2 w-80 overflow-hidden rounded-lg bg-white shadow-xl">
162
+ <div className="border-b border-gray-200 p-4">
163
+ <h3 className="text-sm font-semibold text-gray-900">Notifications</h3>
164
+ </div>
165
+ <div className="p-8 text-center">
166
+ <Bell className="mx-auto mb-3 size-12 text-gray-300" aria-hidden />
167
+ <p className="text-sm text-gray-500">No new notifications</p>
168
+ </div>
169
+ </div>
170
+ </>
171
+ )}
172
+ </div>
106
173
  );
107
174
  }
@@ -29,7 +29,7 @@ export default function ChangePassword() {
29
29
  // [Dev Note] Custom Apex Endpoint: /auth/change-password
30
30
  // You must ensure this Apex class exists in your org
31
31
  const sdk = await getDataSDK();
32
- const response = await sdk.fetch!("/sfdcapi/services/apexrest/auth/change-password", {
32
+ const response = await sdk.fetch!("/services/apexrest/auth/change-password", {
33
33
  method: "POST",
34
34
  body: JSON.stringify({
35
35
  currentPassword: formFieldValues.currentPassword,
@@ -25,7 +25,7 @@ export default function ForgotPassword() {
25
25
  // [Dev Note] Custom Apex Endpoint: /auth/forgot-password
26
26
  // You must ensure this Apex class exists in your org
27
27
  const sdk = await getDataSDK();
28
- const response = await sdk.fetch!("/sfdcapi/services/apexrest/auth/forgot-password", {
28
+ const response = await sdk.fetch!("/services/apexrest/auth/forgot-password", {
29
29
  method: "POST",
30
30
  body: JSON.stringify({ username: value.username.trim() }),
31
31
  headers: {
@@ -27,11 +27,11 @@ export default function Login() {
27
27
  try {
28
28
  // [Dev Note] Salesforce Integration:
29
29
  // We use the Data SDK fetch to make an authenticated (or guest) call to Salesforce.
30
- // "/sfdcapi/services/apexrest/auth/login" refers to a custom Apex REST resource.
30
+ // "/services/apexrest/auth/login" refers to a custom Apex REST resource.
31
31
  // You must ensure this Apex class exists in your org and handles the login logic
32
32
  // (e.g., creating a session or returning a token).
33
33
  const sdk = await getDataSDK();
34
- const response = await sdk.fetch!("/sfdcapi/services/apexrest/auth/login", {
34
+ const response = await sdk.fetch!("/services/apexrest/auth/login", {
35
35
  method: "POST",
36
36
  body: JSON.stringify({
37
37
  email: value.email.trim().toLowerCase(),
@@ -43,12 +43,12 @@ export default function Register() {
43
43
  try {
44
44
  // [Dev Note] Salesforce Integration:
45
45
  // We use the Data SDK fetch to make an authenticated (or guest) call to Salesforce.
46
- // "/sfdcapi/services/apexrest/auth/register" refers to a custom Apex Class exposed as a REST resource.
46
+ // "/services/apexrest/auth/register" refers to a custom Apex Class exposed as a REST resource.
47
47
  // You must ensure this Apex class exists in your org and handles registration
48
48
  // (e.g., duplicate checks and user creation such as Site.createExternalUser).
49
49
  const { confirmPassword, ...request } = formFieldValues;
50
50
  const sdk = await getDataSDK();
51
- const response = await sdk.fetch!("/sfdcapi/services/apexrest/auth/register", {
51
+ const response = await sdk.fetch!("/services/apexrest/auth/register", {
52
52
  method: "POST",
53
53
  body: JSON.stringify({ request }),
54
54
  headers: {
@@ -26,7 +26,7 @@ export default function ResetPassword() {
26
26
  // [Dev Note] Custom Apex Endpoint: /auth/reset-password
27
27
  // You must ensure this Apex class exists in your org
28
28
  const sdk = await getDataSDK();
29
- const response = await sdk.fetch!("/sfdcapi/services/apexrest/auth/reset-password", {
29
+ const response = await sdk.fetch!("/services/apexrest/auth/reset-password", {
30
30
  method: "POST",
31
31
  body: JSON.stringify({ token, newPassword: value.newPassword }),
32
32
  headers: {
@@ -107,15 +107,6 @@ export const routes: RouteObject[] = [
107
107
  path: "object/Property_Listing__c/:id",
108
108
  element: <PropertyDetails />
109
109
  },
110
- {
111
- path: "maintenance/requests",
112
- element: <Maintenance />,
113
- handle: { showInNavigation: true, label: "Maintenance" }
114
- },
115
- {
116
- path: "application",
117
- element: <Application />
118
- },
119
110
  {
120
111
  path: "contact",
121
112
  element: <Contact />,
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "1.93.0",
3
+ "version": "1.94.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.93.0",
3
+ "version": "1.94.0",
4
4
  "description": "B2C sample app template with app shell",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",
@@ -1,43 +0,0 @@
1
- import { gql } from "@salesforce/sdk-data";
2
- import type { GetUserInfoQuery } from "@/api/graphql-operations-types.js";
3
- import { executeGraphQL } from "@/api/graphqlClient.js";
4
-
5
- const GET_USER_INFO = gql`
6
- query GetUserInfo {
7
- uiapi {
8
- query {
9
- User(first: 1) {
10
- edges {
11
- node {
12
- Id
13
- Name {
14
- value
15
- }
16
- }
17
- }
18
- }
19
- }
20
- }
21
- }
22
- `;
23
-
24
- /**
25
- * Fetches the current user's id and name (for TopBar, etc.).
26
- * Returns null on error or when no user is returned.
27
- */
28
- export async function getUserInfo(): Promise<{ name: string; id: string } | null> {
29
- try {
30
- const data = await executeGraphQL<GetUserInfoQuery>(GET_USER_INFO);
31
- const user = data?.uiapi?.query?.User?.edges?.[0]?.node;
32
- if (user) {
33
- return {
34
- id: user.Id,
35
- name: user.Name?.value ?? "User",
36
- };
37
- }
38
- return null;
39
- } catch (error) {
40
- console.error("Error fetching user info:", error);
41
- return null;
42
- }
43
- }