@salesforce/webapp-template-feature-react-authentication-experimental 1.3.4
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/LICENSE.txt +82 -0
- package/README.md +77 -0
- package/dist/.a4drules/build-validation.md +81 -0
- package/dist/.a4drules/code-quality.md +150 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-explore-graphql-schema.md +227 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-mutationquery.md +211 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-readquery.md +185 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-guide-graphql.md +205 -0
- package/dist/.a4drules/graphql/tools/schemas/shared.graphqls +1150 -0
- package/dist/.a4drules/graphql.md +98 -0
- package/dist/.a4drules/images.md +13 -0
- package/dist/.a4drules/react.md +361 -0
- package/dist/.a4drules/react_image_processing.md +45 -0
- package/dist/.a4drules/typescript.md +224 -0
- package/dist/.forceignore +15 -0
- package/dist/.husky/pre-commit +4 -0
- package/dist/.prettierignore +11 -0
- package/dist/.prettierrc +17 -0
- package/dist/CHANGELOG.md +19 -0
- package/dist/README.md +18 -0
- package/dist/config/project-scratch-def.json +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/.prettierignore +9 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/.prettierrc +11 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/eslint.config.js +113 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/feature-react-authentication.webapplication-meta.xml +7 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/index.html +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/package.json +42 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/api/graphql-operations-types.ts +127 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/api/utils/query/highRevenueAccountsQuery.graphql +29 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/app.tsx +16 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/appLayout.tsx +9 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/assets/icons/book.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/assets/icons/copy.svg +4 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/assets/icons/rocket.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/assets/icons/star.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/assets/images/codey-1.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/assets/images/codey-2.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/assets/images/codey-3.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/assets/images/vibe-codey.svg +194 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/alerts/status-alert.tsx +45 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/authentication-route.tsx +21 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/private-route.tsx +36 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/footers/footer-link.tsx +36 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/forms/auth-form.tsx +79 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/forms/submit-button.tsx +49 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/layout/card-layout.tsx +23 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/layout/centered-page-layout.tsx +73 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/layout/loading-page.tsx +46 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/alert.tsx +65 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/button.tsx +56 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/card.tsx +77 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/field.tsx +111 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/index.ts +71 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/input.tsx +19 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/label.tsx +19 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/pagination.tsx +99 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/select.tsx +151 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/skeleton.tsx +7 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/spinner.tsx +21 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/table.tsx +114 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/tabs.tsx +115 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/context/AuthContext.tsx +83 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/hooks/form.tsx +116 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/lib/utils.ts +6 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/About.tsx +12 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/ChangePassword.tsx +105 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/ForgotPassword.tsx +67 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Home.tsx +12 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Login.tsx +84 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/NotFound.tsx +18 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Profile.tsx +146 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Register.tsx +117 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/ResetPassword.tsx +101 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/routes.tsx +77 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/styles/global.css +94 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/utils/authenticationConfig.ts +52 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/utils/helpers.ts +161 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/tsconfig.json +36 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/tsconfig.node.json +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/vite-env.d.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/vite.config.ts +82 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/vitest-env.d.ts +2 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/vitest.config.ts +11 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/vitest.setup.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/webapplication.json +7 -0
- package/dist/jest.config.js +6 -0
- package/dist/package.json +37 -0
- package/dist/scripts/apex/hello.apex +10 -0
- package/dist/scripts/soql/account.soql +6 -0
- package/dist/sfdx-project.json +12 -0
- package/package.json +31 -0
package/dist/force-app/main/default/webapplications/feature-react-authentication/src/routes.tsx
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { RouteObject } from 'react-router';
|
|
2
|
+
import AppLayout from './appLayout';
|
|
3
|
+
import Home from '@/pages/Home';
|
|
4
|
+
import About from './pages/About';
|
|
5
|
+
import NotFound from './pages/NotFound';
|
|
6
|
+
import Login from "./pages/Login";
|
|
7
|
+
import Register from "./pages/Register";
|
|
8
|
+
import ForgotPassword from "./pages/ForgotPassword";
|
|
9
|
+
import ResetPassword from "./pages/ResetPassword";
|
|
10
|
+
import Profile from "./pages/Profile";
|
|
11
|
+
import ChangePassword from "./pages/ChangePassword";
|
|
12
|
+
import AuthenticationRoute from "./components/auth/authentication-route";
|
|
13
|
+
import PrivateRoute from "./components/auth/private-route";
|
|
14
|
+
import { ROUTES } from "./utils/authenticationConfig";
|
|
15
|
+
|
|
16
|
+
export const routes: RouteObject[] = [
|
|
17
|
+
{
|
|
18
|
+
path: "/",
|
|
19
|
+
element: <AppLayout />,
|
|
20
|
+
children: [
|
|
21
|
+
{
|
|
22
|
+
index: true,
|
|
23
|
+
element: <Home />,
|
|
24
|
+
handle: { showInNavigation: true, label: 'Home' }
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: 'about',
|
|
28
|
+
element: <About />,
|
|
29
|
+
handle: { showInNavigation: true, label: 'About' }
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: '*',
|
|
33
|
+
element: <NotFound />
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
element: <AuthenticationRoute />,
|
|
37
|
+
children: [
|
|
38
|
+
{
|
|
39
|
+
path: ROUTES.LOGIN.PATH,
|
|
40
|
+
element: <Login />,
|
|
41
|
+
handle: { showInNavigation: true, label: "Login", title: ROUTES.LOGIN.TITLE }
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: ROUTES.REGISTER.PATH,
|
|
45
|
+
element: <Register />,
|
|
46
|
+
handle: { showInNavigation: false, title: ROUTES.REGISTER.TITLE }
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
path: ROUTES.FORGOT_PASSWORD.PATH,
|
|
50
|
+
element: <ForgotPassword />,
|
|
51
|
+
handle: { showInNavigation: false, title: ROUTES.FORGOT_PASSWORD.TITLE }
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
path: ROUTES.RESET_PASSWORD.PATH,
|
|
55
|
+
element: <ResetPassword />,
|
|
56
|
+
handle: { showInNavigation: false, title: ROUTES.RESET_PASSWORD.TITLE }
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
element: <PrivateRoute />,
|
|
62
|
+
children: [
|
|
63
|
+
{
|
|
64
|
+
path: ROUTES.PROFILE.PATH,
|
|
65
|
+
element: <Profile />,
|
|
66
|
+
handle: { showInNavigation: true, label: "Profile", title: ROUTES.PROFILE.TITLE }
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
path: ROUTES.CHANGE_PASSWORD.PATH,
|
|
70
|
+
element: <ChangePassword />,
|
|
71
|
+
handle: { showInNavigation: false, title: ROUTES.CHANGE_PASSWORD.TITLE }
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@layer base {
|
|
3
|
+
:root {
|
|
4
|
+
/* browser system elements (scrollbars, etc) */
|
|
5
|
+
color-scheme: light;
|
|
6
|
+
|
|
7
|
+
--background: oklch(1 0 0);
|
|
8
|
+
--foreground: oklch(0.145 0 0);
|
|
9
|
+
--card: oklch(1 0 0);
|
|
10
|
+
--card-foreground: oklch(0.145 0 0);
|
|
11
|
+
--popover: oklch(1 0 0);
|
|
12
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
13
|
+
--primary: oklch(0.205 0 0);
|
|
14
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
15
|
+
--secondary: oklch(0.97 0 0);
|
|
16
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
17
|
+
--muted: oklch(0.97 0 0);
|
|
18
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
19
|
+
--accent: oklch(0.97 0 0);
|
|
20
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
21
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
22
|
+
/* 1. Maintenance: Added missing token for accessible text on red backgrounds */
|
|
23
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
24
|
+
--border: oklch(0.922 0 0);
|
|
25
|
+
--input: oklch(0.922 0 0);
|
|
26
|
+
--ring: oklch(0.205 0 0);
|
|
27
|
+
--radius: 0.625rem;
|
|
28
|
+
--spacing: 0.25rem;
|
|
29
|
+
}
|
|
30
|
+
.dark {
|
|
31
|
+
/* browser system elements */
|
|
32
|
+
color-scheme: dark;
|
|
33
|
+
|
|
34
|
+
--background: oklch(0.145 0 0);
|
|
35
|
+
--foreground: oklch(0.985 0 0);
|
|
36
|
+
--card: oklch(0.145 0 0);
|
|
37
|
+
--card-foreground: oklch(0.985 0 0);
|
|
38
|
+
--popover: oklch(0.145 0 0);
|
|
39
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
40
|
+
--primary: oklch(0.985 0 0);
|
|
41
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
42
|
+
--secondary: oklch(0.269 0 0);
|
|
43
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
44
|
+
--muted: oklch(0.269 0 0);
|
|
45
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
46
|
+
--accent: oklch(0.269 0 0);
|
|
47
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
48
|
+
--destructive: oklch(0.396 0.141 25.723);
|
|
49
|
+
/* Dark mode text for destructive actions */
|
|
50
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
51
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
52
|
+
}
|
|
53
|
+
/* 2. Accessibility: Global reset for users who prefer reduced motion */
|
|
54
|
+
@media (prefers-reduced-motion: reduce) {
|
|
55
|
+
* {
|
|
56
|
+
animation-duration: 0.01ms !important;
|
|
57
|
+
animation-iteration-count: 1 !important;
|
|
58
|
+
transition-duration: 0.01ms !important;
|
|
59
|
+
scroll-behavior: auto !important;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
* {
|
|
63
|
+
@apply border-[var(--border)];
|
|
64
|
+
}
|
|
65
|
+
body {
|
|
66
|
+
@apply bg-[var(--background)] text-[var(--foreground)] antialiased;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@theme inline {
|
|
71
|
+
--color-background: var(--background);
|
|
72
|
+
--color-foreground: var(--foreground);
|
|
73
|
+
--color-card: var(--card);
|
|
74
|
+
--color-card-foreground: var(--card-foreground);
|
|
75
|
+
--color-popover: var(--popover);
|
|
76
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
77
|
+
--color-primary: var(--primary);
|
|
78
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
79
|
+
--color-secondary: var(--secondary);
|
|
80
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
81
|
+
--color-muted: var(--muted);
|
|
82
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
83
|
+
--color-accent: var(--accent);
|
|
84
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
85
|
+
--color-destructive: var(--destructive);
|
|
86
|
+
--color-border: var(--border);
|
|
87
|
+
--color-input: var(--input);
|
|
88
|
+
--color-ring: var(--ring);
|
|
89
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
90
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
91
|
+
--radius-lg: var(--radius);
|
|
92
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
93
|
+
--spacing: var(--spacing);
|
|
94
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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;
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
* UI API Record response structure.
|
|
117
|
+
*/
|
|
118
|
+
export type RecordResponse = {
|
|
119
|
+
fields: Record<
|
|
120
|
+
string,
|
|
121
|
+
{
|
|
122
|
+
value: string;
|
|
123
|
+
}
|
|
124
|
+
>;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* [Dev Note] The UI API returns a complex nested structure.
|
|
129
|
+
* This helper flattens it to a simple object for easier form binding.
|
|
130
|
+
* Flattens { fields: { FieldName: { displayValue, value } } } to { FieldName: value }
|
|
131
|
+
*
|
|
132
|
+
* @param data - The RecordResponse with field objects.
|
|
133
|
+
* @throws {Error} If data is not a valid RecordResponse.
|
|
134
|
+
* @returns Flattened object with field values.
|
|
135
|
+
*/
|
|
136
|
+
export function flattenUiApiRecord<T>(data: RecordResponse): T {
|
|
137
|
+
if (!data?.fields) {
|
|
138
|
+
throw new Error(parseApiResponseError(data));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return Object.fromEntries(
|
|
142
|
+
Object.entries(data.fields).map(([key, field]) => [key, field?.value ?? null]),
|
|
143
|
+
) as T;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* [Dev Note] Salesforce APIs may return errors as an array or a single object.
|
|
148
|
+
* This helper standardizes the extraction of the error message string.
|
|
149
|
+
*
|
|
150
|
+
* @param data - The response data.
|
|
151
|
+
* @param fallbackError - Fallback error message if response doesn't have a message property
|
|
152
|
+
* @returns The error message string
|
|
153
|
+
*/
|
|
154
|
+
function parseApiResponseError(
|
|
155
|
+
data: any,
|
|
156
|
+
fallbackError: string = "An unknown error occurred",
|
|
157
|
+
): string {
|
|
158
|
+
return (
|
|
159
|
+
data?.message || data?.error || (Array.isArray(data) ? data[0]?.message : null) || fallbackError
|
|
160
|
+
);
|
|
161
|
+
}
|
package/dist/force-app/main/default/webapplications/feature-react-authentication/tsconfig.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
|
|
23
|
+
/* Path mapping */
|
|
24
|
+
"baseUrl": ".",
|
|
25
|
+
"paths": {
|
|
26
|
+
"@/*": ["./src/*"],
|
|
27
|
+
"@api/*": ["./src/api/*"],
|
|
28
|
+
"@components/*": ["./src/components/*"],
|
|
29
|
+
"@utils/*": ["./src/utils/*"],
|
|
30
|
+
"@styles/*": ["./src/styles/*"],
|
|
31
|
+
"@assets/*": ["./src/assets/*"]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"include": ["src", "vite-env.d.ts", "vitest-env.d.ts"],
|
|
35
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
|
36
|
+
}
|
package/dist/force-app/main/default/webapplications/feature-react-authentication/tsconfig.node.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"composite": true,
|
|
4
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"outDir": "./build"
|
|
11
|
+
},
|
|
12
|
+
"include": ["vite.config.ts"]
|
|
13
|
+
}
|
package/dist/force-app/main/default/webapplications/feature-react-authentication/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
package/dist/force-app/main/default/webapplications/feature-react-authentication/vite.config.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
6
|
+
import salesforce from '@salesforce/vite-plugin-webapp-experimental';
|
|
7
|
+
|
|
8
|
+
export default defineConfig(({ mode }) => {
|
|
9
|
+
return {
|
|
10
|
+
plugins: [tailwindcss(), react(), salesforce()],
|
|
11
|
+
|
|
12
|
+
// Build configuration for MPA
|
|
13
|
+
build: {
|
|
14
|
+
outDir: resolve(__dirname, 'dist'),
|
|
15
|
+
assetsDir: 'assets',
|
|
16
|
+
sourcemap: false,
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// Resolve aliases (shared between build and test)
|
|
20
|
+
resolve: {
|
|
21
|
+
alias: {
|
|
22
|
+
'@': path.resolve(__dirname, './src'),
|
|
23
|
+
'@api': path.resolve(__dirname, './src/api'),
|
|
24
|
+
'@components': path.resolve(__dirname, './src/components'),
|
|
25
|
+
'@utils': path.resolve(__dirname, './src/utils'),
|
|
26
|
+
'@styles': path.resolve(__dirname, './src/styles'),
|
|
27
|
+
'@assets': path.resolve(__dirname, './src/assets'),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// Vitest configuration
|
|
32
|
+
test: {
|
|
33
|
+
// Override root for tests (build uses src/pages as root)
|
|
34
|
+
root: resolve(__dirname),
|
|
35
|
+
|
|
36
|
+
// Use jsdom environment for React component testing
|
|
37
|
+
environment: 'jsdom',
|
|
38
|
+
|
|
39
|
+
// Setup files to run before each test
|
|
40
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
41
|
+
|
|
42
|
+
// Global test patterns
|
|
43
|
+
include: [
|
|
44
|
+
'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
|
45
|
+
'src/**/__tests__/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
|
46
|
+
],
|
|
47
|
+
|
|
48
|
+
// Coverage configuration
|
|
49
|
+
coverage: {
|
|
50
|
+
provider: 'v8',
|
|
51
|
+
reporter: ['text', 'html', 'clover', 'json'],
|
|
52
|
+
exclude: [
|
|
53
|
+
'node_modules/',
|
|
54
|
+
'src/test/',
|
|
55
|
+
'src/**/*.d.ts',
|
|
56
|
+
'src/main.tsx',
|
|
57
|
+
'src/vite-env.d.ts',
|
|
58
|
+
'src/components/**/index.ts',
|
|
59
|
+
'**/*.config.ts',
|
|
60
|
+
'build/',
|
|
61
|
+
'dist/',
|
|
62
|
+
'coverage/',
|
|
63
|
+
'eslint.config.js',
|
|
64
|
+
],
|
|
65
|
+
thresholds: {
|
|
66
|
+
global: {
|
|
67
|
+
branches: 85,
|
|
68
|
+
functions: 85,
|
|
69
|
+
lines: 85,
|
|
70
|
+
statements: 85,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Test timeout
|
|
76
|
+
testTimeout: 10000,
|
|
77
|
+
|
|
78
|
+
// Globals for easier testing
|
|
79
|
+
globals: true,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
package/dist/force-app/main/default/webapplications/feature-react-authentication/vitest.setup.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@salesforce/webapp-template-base-sfdx-project-experimental",
|
|
3
|
+
"version": "1.3.4",
|
|
4
|
+
"description": "Base SFDX project template",
|
|
5
|
+
"private": true,
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "echo 'No build required for base-sfdx-project'",
|
|
11
|
+
"clean": "echo 'No clean required for base-sfdx-project'",
|
|
12
|
+
"lint": "eslint **/{aura,lwc}/**/*.js",
|
|
13
|
+
"test": "npm run test:unit",
|
|
14
|
+
"test:unit": "sfdx-lwc-jest -- --passWithNoTests",
|
|
15
|
+
"test:unit:watch": "sfdx-lwc-jest --watch",
|
|
16
|
+
"test:unit:debug": "sfdx-lwc-jest --debug",
|
|
17
|
+
"test:unit:coverage": "sfdx-lwc-jest --coverage",
|
|
18
|
+
"prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"",
|
|
19
|
+
"prettier:verify": "prettier --check \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"",
|
|
20
|
+
"precommit": "lint-staged"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@lwc/eslint-plugin-lwc": "^2.0.0",
|
|
24
|
+
"@prettier/plugin-xml": "^3.2.2",
|
|
25
|
+
"@salesforce/eslint-config-lwc": "^3.2.3",
|
|
26
|
+
"@salesforce/eslint-plugin-aura": "^2.0.0",
|
|
27
|
+
"@salesforce/eslint-plugin-lightning": "^1.0.0",
|
|
28
|
+
"@salesforce/sfdx-lwc-jest": "^7.0.1",
|
|
29
|
+
"eslint": "8.57.1",
|
|
30
|
+
"eslint-plugin-import": "^2.25.4",
|
|
31
|
+
"eslint-plugin-jest": "^28.8.1",
|
|
32
|
+
"husky": "^9.1.5",
|
|
33
|
+
"lint-staged": "^15.1.0",
|
|
34
|
+
"prettier": "^3.1.0",
|
|
35
|
+
"prettier-plugin-apex": "^2.0.1"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Use .apex files to store anonymous Apex.
|
|
2
|
+
// You can execute anonymous Apex in VS Code by selecting the
|
|
3
|
+
// apex text and running the command:
|
|
4
|
+
// SFDX: Execute Anonymous Apex with Currently Selected Text
|
|
5
|
+
// You can also execute the entire file by running the command:
|
|
6
|
+
// SFDX: Execute Anonymous Apex with Editor Contents
|
|
7
|
+
|
|
8
|
+
string tempvar = 'Enter_your_name_here';
|
|
9
|
+
System.debug('Hello World!');
|
|
10
|
+
System.debug('My name is ' + tempvar);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@salesforce/webapp-template-feature-react-authentication-experimental",
|
|
3
|
+
"version": "1.3.4",
|
|
4
|
+
"description": "Authentication feature for web applications",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "npx tsx ../../cli/src/index.ts apply-patches packages/template/feature/feature-react-authentication packages/template/base-app/base-react-app packages/template/feature/feature-react-authentication/dist --reset --skip-dependency-changes",
|
|
17
|
+
"clean": "rm -rf dist",
|
|
18
|
+
"dev": "cd dist/force-app/main/default/webapplications/feature-react-authentication && npm install && npm run dev",
|
|
19
|
+
"watch": "npx tsx ../../cli/src/index.ts watch-patches packages/template/feature/feature-react-authentication packages/template/base-app/base-react-app packages/template/feature/feature-react-authentication/dist"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@salesforce/webapp-experimental": "^1.3.4",
|
|
23
|
+
"@tanstack/react-form": "^1.27.7",
|
|
24
|
+
"@types/react": "^19.2.7",
|
|
25
|
+
"@types/react-dom": "^19.2.3",
|
|
26
|
+
"react-dom": "^19.2.1",
|
|
27
|
+
"react-router": "^7.10.1",
|
|
28
|
+
"vite": "^7.3.1"
|
|
29
|
+
},
|
|
30
|
+
"gitHead": "89734b9d61484c58ef2c2bf9dce6ef51d9e423f4"
|
|
31
|
+
}
|