;
+}
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/Login.tsx b/openam-ui/openam-ui-js-sdk/src/lib/Login.tsx
new file mode 100644
index 0000000000..0fe4e7844b
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/Login.tsx
@@ -0,0 +1,121 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+
+import { useEffect, useState } from "react";
+import { LoginService } from "./loginService";
+import type { AuthData, AuthError, AuthResponse, SuccessfulAuth } from "./types";
+import { getConfig } from "./config";
+import { useNavigate, useSearchParams } from "react-router";
+
+const config = getConfig();
+
+export type LoginProps = {
+ loginService: LoginService;
+}
+
+const Login: React.FC = ({ loginService }) => {
+
+ const navigate = useNavigate();
+
+ const [searchParams] = useSearchParams();
+
+ const realm = searchParams.get('realm')
+ const service = searchParams.get('service')
+
+ function isAuthError(response: unknown): response is AuthError {
+ return typeof response === 'object' && response !== null && 'code' in response && 'message' in response;
+ }
+
+ function isAuthData(response: unknown): response is AuthData {
+ return typeof response === 'object' && response !== null && 'authId' in response
+ && 'callbacks' in response && Array.isArray(response.callbacks);
+ }
+
+ function isSuccessfulAuth(response: unknown): response is SuccessfulAuth {
+ return typeof response === 'object' && response !== null && 'tokenId' in response
+ && 'successUrl' in response && typeof response.tokenId === 'string' && typeof response.successUrl === 'string';
+ }
+
+ const doRedirect = (url: string) => {
+ const absoluteUrlPattern = /^(?:[a-z+]+:)?\/\//i;
+ if(absoluteUrlPattern.test(url)) {
+ window.location.href = url;
+ } else {
+ window.location.href = config.openamServer.concat(url)
+ }
+ }
+
+ const successfulAuthHandler = async (successfulAuth : SuccessfulAuth) => {
+ if(config.redirectOnSuccessfulLogin){
+ doRedirect(successfulAuth.successUrl);
+ return;
+ }
+ navigate('/')
+ }
+
+ function handleAuthResponse(response: AuthResponse) {
+ if (isAuthData(response)) {
+ setAuthData(response)
+ } else if (isAuthError(response)) {
+ setAuthError(response)
+ } else if (isSuccessfulAuth(response)) {
+ successfulAuthHandler(response);
+ } else {
+ console.error("Unknown response format", response);
+ }
+ }
+
+ const [authData, setAuthData] = useState(null);
+
+ const [authError, setAuthError] = useState(null);
+
+ useEffect(() => {
+ const initAuth = async () => {
+ const authResponse = await loginService.init(realm, service)
+ handleAuthResponse(authResponse);
+ }
+ initAuth();
+
+ }, [loginService, realm, service, navigate])
+
+ const setCallbackValue = (i: number, val: string | number) => {
+ if (!authData) {
+ return;
+ }
+ const newAuthData = loginService.setCallbackValue(i, val, authData);
+ setAuthData(newAuthData);
+ }
+
+ const doLogin = async (action: string) => {
+ if (!authData) {
+ return
+ }
+
+ const newAuthData = loginService.setConfirmationActionValue(action, authData);
+
+ const authResponse = await loginService.submitCallbacks(newAuthData, realm, service)
+
+ handleAuthResponse(authResponse);
+ }
+ if(authError) {
+ return { navigate('/')}} />
+ } else if (authData) {
+ return
+ }
+ return <>Loading...>
+}
+
+export default Login
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/NotFoundPage.tsx b/openam-ui/openam-ui-js-sdk/src/lib/NotFoundPage.tsx
new file mode 100644
index 0000000000..48f1bffd31
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/NotFoundPage.tsx
@@ -0,0 +1,19 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025-2026 3A Systems LLC.
+ */
+
+export default function NotFoundPage() {
+ return
404 — Page Not Found
;
+}
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/OpenAMUI.tsx b/openam-ui/openam-ui-js-sdk/src/lib/OpenAMUI.tsx
new file mode 100644
index 0000000000..0974d102b9
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/OpenAMUI.tsx
@@ -0,0 +1,24 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025-2026 3A Systems LLC.
+ */
+
+import { RouterProvider } from "react-router";
+import { getRouter } from "./router";
+
+const OpenAMUI: React.FC = () => {
+ return
+};
+
+export default OpenAMUI;
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/User.tsx b/openam-ui/openam-ui-js-sdk/src/lib/User.tsx
new file mode 100644
index 0000000000..296a062337
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/User.tsx
@@ -0,0 +1,86 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+
+import { useEffect, useState } from "react";
+import { getConfig } from "./config";
+import type { UserService } from "./userService";
+import type { UserAuthData, UserData } from "./types";
+import { useNavigate } from "react-router";
+
+const config = getConfig();
+
+export type UserProps = {
+ userService: UserService;
+}
+
+const User: React.FC = ({ userService }) => {
+ const navigate = useNavigate();
+
+ const [userAuthData, setUserAuthData] = useState(null);
+
+ const [userData, setUserData] = useState(null);
+
+ useEffect(() => {
+ const initAuth = async () => {
+ const newUserAuthData = await userService.getUserIdFromSession()
+ if(!newUserAuthData) {
+ navigate('/login')
+ }
+ setUserAuthData(newUserAuthData);
+ }
+ initAuth();
+ }, [userService, navigate])
+
+ useEffect(() => {
+ if(!userAuthData) {
+ return
+ }
+ const fetchUserData = async () => {
+ const data = await userService.getUserData(userAuthData.id, userAuthData.realm);
+ setUserData(data);
+ }
+ fetchUserData();
+ }, [userAuthData, userService])
+
+ if (!userData) {
+ return
Loading user data...
; //TODO add customizable loading component
+ }
+
+
+ const saveHandler = async () => {
+ if (!userData || !userAuthData) {
+ return;
+ }
+ const data = await userService.saveUserData(userAuthData.id, userAuthData.realm, userData);
+ setUserData(data);
+ };
+
+ const savePassword = async(password: string) => {
+ if(!userAuthData) {
+ return;
+ }
+ if(!password) {
+ throw new Error("password is empty")
+ }
+
+ await userService.savePassword(userAuthData.id, userAuthData.realm, password)
+ }
+
+
+ return
+}
+
+export default User;
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/__tests__/mocks.ts b/openam-ui/openam-ui-js-sdk/src/lib/__tests__/mocks.ts
new file mode 100644
index 0000000000..50ef9d72a3
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/__tests__/mocks.ts
@@ -0,0 +1,178 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+import type { AuthData, AuthError, SuccessfulAuth, UserAuthData, UserData } from "../types"
+
+const authDataJSON = `{
+ "authId": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJvdGsiOiJsa21mODI5dHEzbmhraDNyNmVsbGZtYWpybCIsInJlYWxtIjoiZGM9b3BlbmFtLGRjPW9wZW5pZGVudGl0eXBsYXRmb3JtLGRjPW9yZyIsInNlc3Npb25JZCI6IkFRSUM1d00yTFk0U2ZjekloNTRQLTZ1czRod0tSa09ibWFKa251U0p3SUxNYi1VLipBQUpUU1FBQ01ERUFBbE5MQUJNMk56VTVOVEF5T1RrNU5UUXpOemM0T1RZNEFBSlRNUUFBKiJ9.0lYgF063co7bcg_-xbabvrZponm7NMq3s-IeYPaf9Js",
+ "template": "",
+ "stage": "DataStore1",
+ "header": "Sign in to OpenAM",
+ "infoText": [
+ "",
+ ""
+ ],
+ "callbacks": [
+ {
+ "type": "NameCallback",
+ "output": [
+ {
+ "name": "prompt",
+ "value": "User Name:"
+ }
+ ],
+ "input": [
+ {
+ "name": "IDToken1",
+ "value": "demo"
+ }
+ ]
+ },
+ {
+ "type": "PasswordCallback",
+ "output": [
+ {
+ "name": "prompt",
+ "value": "Password:"
+ }
+ ],
+ "input": [
+ {
+ "name": "IDToken2",
+ "value": "changeit"
+ }
+ ]
+ },
+ {
+ "type": "ConfirmationCallback",
+ "output": [
+ {
+ "name": "prompt",
+ "value": ""
+ },
+ {
+ "name": "messageType",
+ "value": 0
+ },
+ {
+ "name": "options",
+ "value": [
+ "Register device",
+ "Skip this step"
+ ]
+ },
+ {
+ "name": "optionType",
+ "value": -1
+ },
+ {
+ "name": "defaultOption",
+ "value": 0
+ }
+ ],
+ "input": [
+ {
+ "name": "IDToken3",
+ "value": "1"
+ }
+ ]
+ }
+ ]
+}`
+
+const successfulAuthJSON = `{
+ "tokenId": "AQIC5wM2LY4SfcwIaAQY6dwlk4xEQjX9v59vw3gRzpGwfTI.*AAJTSQACMDEAAlNLABM2NDI1MzUyMDYwODgwODYyNzkyAAJTMQAA*",
+ "successUrl": "/openam/console",
+ "realm": "/"
+}`
+
+
+const authErrorJSON = `{"code":401,"reason":"Unauthorized","message":"Authentication Failed"}`
+
+const userAuthDataJSON = `{
+ "id": "demo",
+ "realm": "/",
+ "dn": "id=demo,ou=user,dc=openam,dc=openidentityplatform,dc=org",
+ "successURL": "/openam/console",
+ "fullLoginURL": "/openam/UI/Login?realm=%2F"
+}`
+
+const userDataJSON = `{
+ "username": "demo",
+ "realm": "/",
+ "uid": [
+ "demo"
+ ],
+ "universalid": [
+ "id=demo,ou=user,dc=openam,dc=openidentityplatform,dc=org"
+ ],
+ "oath2faEnabled": [
+ "1"
+ ],
+ "objectClass": [
+ "iplanet-am-managed-person",
+ "inetuser",
+ "sunFederationManagerDataStore",
+ "sunFMSAML2NameIdentifier",
+ "devicePrintProfilesContainer",
+ "inetorgperson",
+ "sunIdentityServerLibertyPPService",
+ "iPlanetPreferences",
+ "pushDeviceProfilesContainer",
+ "iplanet-am-user-service",
+ "forgerock-am-dashboard-service",
+ "organizationalperson",
+ "top",
+ "kbaInfoContainer",
+ "sunAMAuthAccountLockout",
+ "person",
+ "oathDeviceProfilesContainer",
+ "iplanet-am-auth-configuration-service"
+ ],
+ "inetUserStatus": [
+ "Active"
+ ],
+ "dn": [
+ "uid=demo,ou=people,dc=openam,dc=openidentityplatform,dc=org"
+ ],
+ "sn": [
+ "John"
+ ],
+ "cn": [
+ "John Doe"
+ ],
+ "mail": [
+ "john.doe@example.org"
+ ],
+ "telephoneNumber": [
+ "+1234567890"
+ ],
+ "createTimestamp": [
+ "20250805142017Z"
+ ],
+ "modifyTimestamp": [
+ "20250925124445Z"
+ ],
+ "roles": [
+ "ui-self-service-user"
+ ]
+}`
+
+export const mockAuthData = JSON.parse(authDataJSON) as AuthData
+export const mockSuccessfulAuth = JSON.parse(successfulAuthJSON) as SuccessfulAuth
+export const mockAuthError = JSON.parse(authErrorJSON) as AuthError
+
+export const mockUserAuthData = JSON.parse(userAuthDataJSON) as UserAuthData
+export const mockUserData = JSON.parse(userDataJSON) as UserData
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultActionElements.test.tsx b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultActionElements.test.tsx
new file mode 100644
index 0000000000..2b70871451
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultActionElements.test.tsx
@@ -0,0 +1,39 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+
+import { render, screen } from '@testing-library/react';
+import { describe, it, expect } from 'vitest';
+import DefaultActionElements from './DefaultActionElements';
+import { mockAuthData } from '../__tests__/mocks';
+
+describe('DefaultActionElements', () => {
+
+ it('renders actions from confirmation callback', () => {
+ render();
+ const registerButton = screen.getByText('Register device')
+ expect(registerButton).toBeInTheDocument();
+
+ const skipButton = screen.getByText('Skip this step')
+ expect(skipButton).toBeInTheDocument();
+ });
+
+ it('renders default action', () => {
+ render();
+ const defaultButton = screen.getByText('Log In')
+ expect(defaultButton).toBeInTheDocument();
+ });
+
+});
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultActionElements.tsx b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultActionElements.tsx
new file mode 100644
index 0000000000..fe2c94c41a
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultActionElements.tsx
@@ -0,0 +1,37 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+
+import type { ActionElements } from "./types";
+
+const DefaultActionElements: ActionElements = ({ callbacks }) => {
+
+ const defaultAction =
+ const callbackIdx = callbacks.findIndex((cb) => (cb.type === 'ConfirmationCallback'));
+ if (callbackIdx < 0) {
+ return defaultAction;
+ }
+ const opts = callbacks[callbackIdx].output.find((o) => (o.name === 'options'))?.value;
+ if (!Array.isArray(opts)) {
+ return defaultAction;
+ }
+
+ return <>{opts.map((o, i) =>
+ )}
+ >;
+}
+
+export default DefaultActionElements
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultCallbackElement.test.tsx b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultCallbackElement.test.tsx
new file mode 100644
index 0000000000..85a43895c0
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultCallbackElement.test.tsx
@@ -0,0 +1,51 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+
+import { fireEvent, render, screen } from '@testing-library/react';
+import { vi, describe, it, expect } from 'vitest';
+import DefaultCallbackElement from './DefaultCallbackElement';
+import { mockAuthData } from '../__tests__/mocks';
+
+describe('DefaultCallbackElement', () => {
+ const setCallbackValue = vi.fn();
+
+ it('renders login input', () => {
+ render();
+
+ const input = screen.getByRole('textbox')
+ expect(input).toBeInTheDocument();
+ expect(input).toHaveAttribute('type', 'text');
+ expect(input).toHaveAttribute('value', 'demo');
+ });
+
+ it('renders password input', () => {
+ const { container } = render();
+
+ const input = container.querySelector("#IDToken2");
+ expect(input).toBeInTheDocument();
+ expect(input).toHaveAttribute('type', 'password');
+ });
+
+ it('changes callback value', () => {
+ const newLogin = 'newLogin';
+ render();
+ const input = screen.getByRole('textbox')
+ expect(input).toBeInTheDocument();
+ fireEvent.change(input, {target: {value: newLogin}});
+ expect(setCallbackValue).toHaveBeenCalledTimes(1);
+ expect(setCallbackValue).toHaveBeenCalledWith(newLogin)
+ });
+});
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultCallbackElement.tsx b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultCallbackElement.tsx
new file mode 100644
index 0000000000..ac942bb9ce
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultCallbackElement.tsx
@@ -0,0 +1,78 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+
+import { useEffect } from "react";
+import type { Callback } from "../types";
+import type { CallbackElement } from "./types";
+
+const ScriptElement: React.FC<{ scriptText: string }> = ({ scriptText }) => {
+ useEffect(() => {
+ const script = document.createElement('script');
+ script.innerHTML = scriptText;
+ document.body.appendChild(script);
+ return () => {
+ if (document.body.contains(script)) {
+ document.body.removeChild(script);
+ }
+ };
+ }, [scriptText]);
+
+ return null; // This component renders nothing in the DOM
+}
+
+const DefaultCallbackElement: CallbackElement = ({ callback, setCallbackValue }) => {
+
+ let inputId;
+ if (callback.input) {
+ inputId = callback.input[0].name;
+ }
+
+ const renderTextOutputCallback = (callback: Callback) => {
+ const propMap = Object.fromEntries(callback.output.map((o) => [o.name, o.value]))
+ const messageType = String(propMap["messageType"] ?? "");
+ const message = propMap['message'] as string
+ switch (messageType) {
+ case "0":
+ case "1":
+ case "2":
+ return
{message}
+ case "4":
+ return ;
+ default:
+ console.log(`unknown message type: ${messageType}`)
+ return <>>;
+ }
+ }
+
+ switch (callback.type) {
+ case "NameCallback":
+ return setCallbackValue(e.target.value)} type="text" name={inputId}
+ value={callback.input[0].value} required={true} />
+ case "PasswordCallback":
+ return setCallbackValue(e.target.value)} type="password" name={inputId}
+ value={callback.input[0].value} required={true} />
+ case "TextOutputCallback":
+ return renderTextOutputCallback(callback)
+ default:
+ return null
+ }
+}
+
+
+
+export default DefaultCallbackElement;
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultErrorForm.test.tsx b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultErrorForm.test.tsx
new file mode 100644
index 0000000000..b858d98e0a
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultErrorForm.test.tsx
@@ -0,0 +1,44 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+
+import { render, screen, fireEvent } from '@testing-library/react';
+import { vi, describe, it, expect } from 'vitest';
+import DefaultErrorForm from './DefaultErrorForm';
+
+
+describe('DefaultErrorForm', () => {
+ const mockResetError = vi.fn();
+
+ const defaultProps = {
+ error: { code: 401, reason: 'Test reason', message: 'Test error message'},
+ resetError: mockResetError
+ }
+
+ it('renders error message and retry button', () => {
+ render();
+
+ expect(screen.getByText('An error occurred')).toBeInTheDocument();
+ expect(screen.getByText('Test error message')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument();
+ });
+
+ it('calls resetError when retry button is clicked', () => {
+ render();
+ const retryButton = screen.getByRole('button', { name: 'Retry' });
+ fireEvent.click(retryButton);
+ expect(mockResetError).toHaveBeenCalledTimes(1);
+ });
+});
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultErrorForm.tsx b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultErrorForm.tsx
new file mode 100644
index 0000000000..808ab7b111
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultErrorForm.tsx
@@ -0,0 +1,30 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025-2026 3A Systems LLC.
+ */
+
+
+import type { ErrorForm } from "./types";
+
+const DefaultErrorForm: ErrorForm = ({ error, resetError }) => {
+ return
+
An error occurred
+
{error?.message}
+
+ resetError()} />
+
+
+}
+
+export default DefaultErrorForm;
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultLoginForm.test.tsx b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultLoginForm.test.tsx
new file mode 100644
index 0000000000..4f590d5327
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultLoginForm.test.tsx
@@ -0,0 +1,77 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+
+import { render, fireEvent } from '@testing-library/react';
+import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
+import DefaultLoginForm from './DefaultLoginForm';
+import { mockAuthData } from '../__tests__/mocks';
+import { getConfig, setConfig, type Config } from '../config';
+import type { ActionElements, CallbackElement } from './types';
+
+describe('DefaultLoginForm', () => {
+ const mockCallbackElement: CallbackElement = vi.fn();
+ const mockActionElements: ActionElements = vi.fn();
+
+ const mockSetCallbackValue = vi.fn();
+ const mockDoLogin = vi.fn()
+
+ let previousConfig: Config;
+
+ beforeEach(() => {
+ previousConfig = getConfig();
+ setConfig({
+ CallbackElement: mockCallbackElement,
+ ActionElements: mockActionElements,
+ });
+ });
+
+ afterEach(() => {
+ setConfig(previousConfig);
+ });
+
+ setConfig({
+ CallbackElement: mockCallbackElement,
+ ActionElements: mockActionElements,
+ })
+
+
+ const defaultProps = {
+ authData: mockAuthData,
+ setCallbackValue: mockSetCallbackValue,
+ doLogin: mockDoLogin
+ }
+
+ it('renders login form', () => {
+ render();
+
+ expect(mockCallbackElement).toHaveBeenCalledTimes(2);
+ expect(mockActionElements).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls doLogin on form submit', () => {
+ const {container} = render();
+
+ const form = container.querySelector('form');
+ if(!form) {
+ expect(form).not.toBeNull();
+ return;
+ }
+
+ expect(form).toBeInTheDocument();
+ fireEvent.submit(form);
+ expect(mockDoLogin).toHaveBeenCalledTimes(1)
+ });
+});
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultLoginForm.tsx b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultLoginForm.tsx
new file mode 100644
index 0000000000..05f55ea19c
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultLoginForm.tsx
@@ -0,0 +1,53 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+
+import type { LoginForm } from "./types";
+import { getConfig } from "../config";
+
+const DefaultLoginForm: LoginForm = ({ authData, setCallbackValue, doLogin }) => {
+
+ const config = getConfig();
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ const submitEvent = e.nativeEvent as SubmitEvent;
+ const submitter = submitEvent.submitter;
+
+ if (submitter instanceof HTMLButtonElement || submitter instanceof HTMLInputElement) {
+ doLogin(submitter.value);
+ } else {
+ doLogin('');
+ }
+
+ }
+
+ return
+
{authData.header}
+
+
+}
+
+export default DefaultLoginForm
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultUserForm.test.tsx b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultUserForm.test.tsx
new file mode 100644
index 0000000000..f93c91c096
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultUserForm.test.tsx
@@ -0,0 +1,202 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025-2026 3A Systems LLC.
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, act } from '@testing-library/react';
+import DefaultUserForm from './DefaultUserForm';
+import { mockUserData } from '../__tests__/mocks';
+import type { UserData } from '../types';
+
+describe('DefaultUserForm', () => {
+
+
+ let defaultProps: {
+ userData: UserData,
+ setUserData: (userData: UserData) => void,
+ saveHandler: () => void,
+ savePasswordHandler: (password: string) => void
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ window.alert = vi.fn();
+
+ const mockSetUserData = vi.fn();
+ const mockSaveHandler = vi.fn();
+ const mockSavePasswordHandler = vi.fn();
+ defaultProps = {
+ userData: mockUserData,
+ setUserData: mockSetUserData,
+ saveHandler: mockSaveHandler,
+ savePasswordHandler: mockSavePasswordHandler
+ };
+
+ });
+
+ it('displays user data correctly in input fields', () => {
+ render(
+
+ );
+
+ expect(screen.getByDisplayValue('demo')).toBeInTheDocument(); // username (readonly)
+ expect(screen.getByDisplayValue('John')).toBeInTheDocument();
+ expect(screen.getByDisplayValue('john.doe@example.org')).toBeInTheDocument();
+ expect(screen.getByDisplayValue('+1234567890')).toBeInTheDocument();
+ });
+
+ it('username field is readonly', () => {
+ render(
+
+ );
+
+ const usernameInput = screen.getByLabelText(/username/i) as HTMLInputElement;
+ expect(usernameInput).toHaveAttribute('readonly');
+ expect(usernameInput.readOnly).toBe(true);
+ });
+
+ it('calls setUserData with updated array value when editable fields change', () => {
+ render(
+
+ );
+
+ const firstNameInput = screen.getByLabelText(/first name/i);
+ fireEvent.change(firstNameInput, { target: { value: 'Jane' } });
+
+ expect(defaultProps.setUserData).toHaveBeenCalledWith({
+ ...mockUserData,
+ givenName: ['Jane'],
+ });
+
+ const emailInput = screen.getByLabelText(/mail/i);
+ fireEvent.change(emailInput, { target: { value: 'jane.doe@example.org' } });
+
+ expect(defaultProps.setUserData).toHaveBeenCalledWith({
+ ...mockUserData,
+ mail: ['jane.doe@example.org'],
+ });
+ });
+
+ it('calls saveHandler and prevents default on form submit', () => {
+ render(
+
+ );
+ const submitButton = screen.getByRole('button', { name: /save/i });
+ fireEvent.click(submitButton);
+ expect(defaultProps.saveHandler).toHaveBeenCalledTimes(1);
+ });
+
+ it('opens set password modal on click', () => {
+ render(
+
+ );
+ const changePasswordButton = screen.getByText('Change Password');
+ expect(changePasswordButton).toBeInTheDocument();
+
+ fireEvent.click(changePasswordButton);
+
+ expect(screen.getByLabelText("New:")).toBeInTheDocument();
+ expect(screen.getByLabelText("Confirm:")).toBeInTheDocument();
+ expect(screen.getByText("Update Password")).toBeInTheDocument();
+ });
+
+ it('validates new and confirm password', () => {
+ render(
+
+ );
+ const changePasswordButton = screen.getByText('Change Password');
+ expect(changePasswordButton).toBeInTheDocument();
+
+ fireEvent.click(changePasswordButton);
+
+ fireEvent.change(screen.getByLabelText('New:'), { target: { value: 'newPass123' } });
+ fireEvent.change(screen.getByLabelText('Confirm:'), { target: { value: 'mismatch123' } });
+
+ fireEvent.click(screen.getByText('Update Password'));
+
+ expect(window.alert).toHaveBeenCalledWith('Passwords do not match.');
+
+ expect(screen.getByLabelText('New:')).toBeInTheDocument();
+ });
+
+ it('saves password', () => {
+ render(
+
+ );
+ const changePasswordButton = screen.getByText('Change Password');
+ expect(changePasswordButton).toBeInTheDocument();
+
+ fireEvent.click(changePasswordButton);
+
+ fireEvent.change(screen.getByLabelText('New:'), { target: { value: 'newPass123' } });
+ fireEvent.change(screen.getByLabelText('Confirm:'), { target: { value: 'newPass123' } });
+ fireEvent.click(screen.getByText('Update Password'));
+
+
+ expect(defaultProps.savePasswordHandler).toHaveBeenCalledTimes(1);
+ });
+
+ describe('Change Password button accessibility', () => {
+ it('has role="button" for assistive technology', () => {
+ render();
+
+ const changePasswordButton = screen.getByRole('button', { name: 'Change Password' });
+ expect(changePasswordButton).toBeInTheDocument();
+ });
+
+ it('is focusable via keyboard (tabIndex=0)', () => {
+ render();
+
+ const changePasswordButton = screen.getByRole('button', { name: 'Change Password' });
+ expect(changePasswordButton).toHaveAttribute('tabindex', '0');
+ });
+
+ it('opens password modal on Enter key press', () => {
+ render();
+
+ act(() => {
+ fireEvent.keyDown(screen.getByRole('button', { name: 'Change Password' }), { key: 'Enter' });
+ });
+
+ expect(screen.getByLabelText('New:')).toBeInTheDocument();
+ expect(screen.getByLabelText('Confirm:')).toBeInTheDocument();
+ expect(screen.getByText('Update Password')).toBeInTheDocument();
+ });
+
+ it('opens password modal on Space key press', () => {
+ render();
+
+ act(() => {
+ fireEvent.keyDown(screen.getByRole('button', { name: 'Change Password' }), { key: ' ' });
+ });
+
+ expect(screen.getByLabelText('New:')).toBeInTheDocument();
+ expect(screen.getByLabelText('Confirm:')).toBeInTheDocument();
+ expect(screen.getByText('Update Password')).toBeInTheDocument();
+ });
+
+ it('does not open password modal on other key presses', () => {
+ render();
+
+ act(() => {
+ fireEvent.keyDown(screen.getByRole('button', { name: 'Change Password' }), { key: 'Tab' });
+ });
+
+ expect(screen.queryByLabelText('New:')).not.toBeInTheDocument();
+ });
+ });
+
+});
\ No newline at end of file
diff --git a/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultUserForm.tsx b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultUserForm.tsx
new file mode 100644
index 0000000000..6c8769b960
--- /dev/null
+++ b/openam-ui/openam-ui-js-sdk/src/lib/components/DefaultUserForm.tsx
@@ -0,0 +1,183 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2025 3A Systems LLC.
+ */
+
+import { useState } from "react";
+import type { UserData } from "../types";
+import type { UserForm } from "./types";
+
+const DefaultUserForm: UserForm = ({ userData, setUserData, saveHandler, savePasswordHandler }) => {
+
+ const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
+
+ //return // Helper to handle string/array fields
+ const handleChange = (key: keyof UserData, value: string) => {
+ setUserData({ ...userData, [key]: [value] });
+ };
+
+
+ return (
+ <>
+