diff --git a/src/common/css/lib/src/curity-theme.css b/src/common/css/lib/src/curity-theme.css
index 572fdbf7..1e7aac86 100644
--- a/src/common/css/lib/src/curity-theme.css
+++ b/src/common/css/lib/src/curity-theme.css
@@ -27,7 +27,7 @@
--well-box-shadow: rgb(0 0 0 / 5%) 0 6px 24px 0, rgb(0 0 0 / 8%) 0 0 0 1px;
--login-symbol-display: flex;
- --login-symbol-size: 140px;
+ --login-symbol-size: 180px;
--login-symbol-margin-top: 2rem;
--login-symbol-margin-bottom: 2rem;
--login-symbol-border-radius: 50%;
diff --git a/src/login-web-app/previewer/Previewer.tsx b/src/login-web-app/previewer/Previewer.tsx
index 88d30e55..b94e125b 100644
--- a/src/login-web-app/previewer/Previewer.tsx
+++ b/src/login-web-app/previewer/Previewer.tsx
@@ -19,6 +19,23 @@ import { formatNextStepData } from '../src/haapi-stepper/feature/stepper/data-fo
import { HaapiStepperContext } from '../src/haapi-stepper/feature/stepper/HaapiStepperContext';
import { type HaapiStepperAPI } from '../src/haapi-stepper/feature/stepper/haapi-stepper.types';
import { HaapiErrorStep } from '../src/haapi-stepper/data-access';
+import { HaapiAppConfigContext } from '../src/shared/feature/app-config/HaapiAppConfigContext';
+import { HaapiAppConfig } from '../src/shared/feature/app-config/types';
+
+const mockAppConfig: HaapiAppConfig = {
+ initialUrl: '',
+ haapi: { clientId: '', tokenEndpoint: '' },
+ theme: {
+ logo: {
+ path: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgMTg4IDU0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDxkZWZzPgogICAgICAgIDxwYXRoIGlkPSJwYXRoLTEiIGQ9Ik0wLDEuMDk2MzYzNjQgTDE3OS4zODgzNjcsMS4wOTYzNjM2NCBMMTc5LjM4ODM2NywzOC4xODE4MTgyIEwwLDM4LjE4MTgxODIgTDAsMS4wOTYzNjM2NCBaIj48L3BhdGg+CiAgICAgICAgPHBhdGggaWQ9InBhdGgtMyIgZD0iTTAsMS4wOTYzNjM2NCBMMTc5LjM4ODM2NywxLjA5NjM2MzY0IEwxNzkuMzg4MzY3LDM4LjE4MTgxODIgTDAsMzguMTgxODE4MiBMMCwxLjA5NjM2MzY0IFoiPjwvcGF0aD4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPgogICAgICAgIDxnIGlkPSJjdXJpdHktbG9nbyIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCI+CiAgICAgICAgICAgIDxnIGlkPSJQYWdlLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQuMDAwMDAwLCA3LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPGcgaWQ9Ikdyb3VwLTMiPgogICAgICAgICAgICAgICAgICAgIDxtYXNrIGlkPSJtYXNrLTIiIHNrZXRjaDpuYW1lPSJDbGlwIDIiIGZpbGw9IndoaXRlIj4KICAgICAgICAgICAgICAgICAgICAgICAgPHVzZSB4bGluazpocmVmPSIjcGF0aC0xIj48L3VzZT4KICAgICAgICAgICAgICAgICAgICA8L21hc2s+CiAgICAgICAgICAgICAgICAgICAgPGcgaWQ9IkNsaXAtMiI+PC9nPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik01MS40NTM3MzQ3LDI0LjYxMjkwOTEgTDUwLjg3NTE2MzMsMjUuMTg3NDU0NSBDNDcuNjA3NjEyMiwyOC40MjIgNDMuNTAwNjczNSwzMC4yNzgzNjM2IDM5LjYwNDk1OTIsMzAuMjc4MzYzNiBDMzMuNDI0MzQ2OSwzMC4yNzgzNjM2IDI4LjkzOTA0MDgsMjUuODEyOTA5MSAyOC45MzkwNDA4LDE5LjY2MiBDMjguOTM5MDQwOCwxMy41MTQ3MjczIDMzLjMxNTk3OTYsOS4wNTEwOTA5MSAzOS4zNDc4MTYzLDkuMDUxMDkwOTEgQzQyLjg0MTI4NTcsOS4wNTEwOTA5MSA0Ni45Nzc2MTIyLDEwLjY2MzgxODIgNDkuODg1MTYzMywxMy4xNTQ3MjczIEw1MC40Njc0MDgyLDEzLjY1ODM2MzYgTDU2LjI0OTQ0OSw3LjY3ODM2MzY0IEw1NS41OTc0MDgyLDcuMTI3NDU0NTUgQzUxLjE1NjE4MzcsMy4zNTI5MDkwOSA0NC44NDg4MzY3LDEuMDk0NzI3MjcgMzguNzI3LDEuMDk0NzI3MjcgQzMxLjk4MjUxMDIsMS4wOTQ3MjcyNyAyNi4yNzAyNjUzLDQuMTMxMDkwOTEgMjIuODc1OTc5Niw4LjkzMjkwOTA5IEwxMi4yMjEwODE2LDguOTMyOTA5MDkgTDExLjU5ODQyODYsMTQuMTI1NjM2NCBMMjAuMzUwNDY5NCwxNC4xMjU2MzY0IEMyMC4wNjk0NDksMTUuMDYzODE4MiAxOS44NzQ3NTUxLDE2LjA0MDE4MTggMTkuNzQyNTEwMiwxNy4wNDM4MTgyIEw2LjY0NjU5MTg0LDE3LjA0MzgxODIgTDYuMDIzOTM4NzgsMjIuMjM0NzI3MyBMMTkuNzQwNjczNSwyMi4yMzQ3MjczIEMxOS44NzEwODE2LDIzLjIzODM2MzYgMjAuMDYzOTM4OCwyNC4yMTY1NDU1IDIwLjMzOTQ0OSwyNS4xNDkyNzI3IEwyMC4zMzAyNjUzLDI1LjE0OTI3MjcgTDE5LjU1MzMyNjUsMjUuMTQ5MjcyNyBMMC42MjM5Mzg3NzYsMjUuMTQ5MjcyNyBMLTAuMDAwNTUxMDIwNDA4LDMwLjM0MiBMMTkuNTUzMzI2NSwzMC4zNDIgTDIwLjMzMDI2NTMsMzAuMzQyIEwyMi44MzAwNjEyLDMwLjM0MiBDMjYuMjI2MTgzNywzNS4xODAxODE4IDMyLjAxMTg5OCwzOC4xODIgMzguOTg1OTc5NiwzOC4xODIgQzQ3Ljc0MzUzMDYsMzguMTgyIDUzLjg0ODgzNjcsMzMuNzQ3NDU0NSA1Ni43MDg2MzI3LDMxLjEwNTYzNjQgTDU3LjMxNjU5MTgsMzAuNTQzODE4MiBMNTEuNDUzNzM0NywyNC42MTI5MDkxIFoiIGlkPSJGaWxsLTEiIGZpbGw9IiM2MjZDODciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIG1hc2s9InVybCgjbWFzay0yKSI+PC9wYXRoPgogICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTUuMTYzMDYxMjIsMTcuMDQzNjM2NCBMMS4zMDQwODE2MywxNy4wNDM2MzY0IEwwLjY4MTQyODU3MSwyMi4yMzQ1NDU1IEw0LjU0MjI0NDksMjIuMjM0NTQ1NSBMNS4xNjMwNjEyMiwxNy4wNDM2MzY0IFoiIGlkPSJGaWxsLTQiIGZpbGw9IiM2MjZDODciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik04Ny40NjIsMjMuOTQ4OTA5MSBDODUuMzUxNTkxOCwyNS44OTggODIuMTQyODE2MywyNy42ODE2MzY0IDc4LjEyMDM2NzMsMjcuNjgxNjM2NCBDNzIuMDEzMjI0NSwyNy42ODE2MzY0IDY3Ljk4ODkzODgsMjMuMzYzNDU0NSA2Ny45ODg5Mzg4LDE4LjAxNDM2MzYgQzY3Ljk4ODkzODgsMTIuNzc4IDcyLjA0MDc3NTUsOC4zNzggNzcuOTc4OTM4OCw4LjM3OCBDODEuMzU0ODU3MSw4LjM3OCA4NC42NzU2NzM1LDkuNjg3MDkwOTEgODYuODcwNTcxNCwxMS41NTQzNjM2IEw4NC4zMzc3MTQzLDE0LjE3MDcyNzMgQzgyLjY0OTc1NTEsMTIuNzIzNDU0NSA4MC4yODU4Nzc2LDExLjgzMjU0NTUgNzguMzE2ODk4LDExLjgzMjU0NTUgQzc0LjYwMzAyMDQsMTEuODMyNTQ1NSA3Mi4yMDk3NTUxLDE0LjY0NTI3MjcgNzIuMjA5NzU1MSwxOC4wNDM0NTQ1IEM3Mi4yMDk3NTUxLDIxLjQ2ODkwOTEgNzQuNjU4MTIyNCwyNC4yNTQzNjM2IDc4LjQ1ODMyNjUsMjQuMjU0MzYzNiBDODAuOTYxNzk1OSwyNC4yNTQzNjM2IDgzLjI5NjI4NTcsMjIuOTQ1MjcyNyA4NC44OTk3NTUxLDIxLjM1OCBMODcuNDYyLDIzLjk0ODkwOTEgWiIgaWQ9IkZpbGwtNiIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTEwNy41MjMzNjcsOC45MzQ3MjcyNyBMMTA3LjUyMzM2NywyMC41NzY1NDU1IEMxMDcuNTIzMzY3LDI1LjExODM2MzYgMTAzLjk3NjYzMywyNy42ODIgOTkuMTA5Mjg1NywyNy42ODIgQzk0LjM4MTUzMDYsMjcuNjgyIDkwLjc1MjE0MjksMjUuMTE4MzYzNiA5MC43NTIxNDI5LDIwLjU3NjU0NTUgTDkwLjc1MjE0MjksOC45MzQ3MjcyNyBMOTQuODAzOTc5Niw4LjkzNDcyNzI3IEw5NC44MDM5Nzk2LDIwLjU3NjU0NTUgQzk0LjgwMzk3OTYsMjIuOTc0NzI3MyA5Ni43NDU0MDgyLDI0LjI1NDcyNzMgOTkuMTA5Mjg1NywyNC4yNTQ3MjczIEMxMDEuNjE0NTkyLDI0LjI1NDcyNzMgMTAzLjQ3MTUzMSwyMi45NzQ3MjczIDEwMy40NzE1MzEsMjAuNTc2NTQ1NSBMMTAzLjQ3MTUzMSw4LjkzNDcyNzI3IEwxMDcuNTIzMzY3LDguOTM0NzI3MjcgWiIgaWQ9IkZpbGwtOCIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTExNy4wMDQ3NzYsMTIuMzA0NzI3MyBMMTE3LjAwNDc3NiwxNy4wNjgzNjM2IEwxMjMuNTg5NDY5LDE3LjA2ODM2MzYgQzEyNC45OTY0MDgsMTcuMDY4MzYzNiAxMjYuMTQ5ODc4LDE2LjA2NDcyNzMgMTI2LjE0OTg3OCwxNC42NzM4MTgyIEMxMjYuMTQ5ODc4LDE0LjA2MTA5MDkgMTI1LjkyMzk1OSwxMy41MDI5MDkxIDEyNS41MDE1MSwxMy4wODQ3MjczIEMxMjQuODU0OTgsMTIuMzg4MzYzNiAxMjQuMDM5NDY5LDEyLjMwNDcyNzMgMTIzLjMwODQ0OSwxMi4zMDQ3MjczIEwxMTcuMDA0Nzc2LDEyLjMwNDcyNzMgWiBNMTEyLjk4MjMyNywyNy4xMjI5MDkxIEwxMTIuOTgyMzI3LDguOTMzODE4MTggTDEyMy4xNjcwMiw4LjkzMzgxODE4IEMxMjUuNzg0MzY3LDguOTMzODE4MTggMTI3LjQ5OTg3OCw5LjY4ODM2MzY0IDEyOC42ODI3MzUsMTAuOTEyIEMxMjkuNjM5NjczLDExLjkxNTYzNjQgMTMwLjIwMTcxNCwxMy4yMjQ3MjczIDEzMC4yMDE3MTQsMTQuNjczODE4MiBDMTMwLjIwMTcxNCwxNy4wMTIgMTI4LjczOTY3MywxOC44MjI5MDkxIDEyNi40ODc4MzcsMTkuNzk3NDU0NSBMMTMwLjc5MzE0MywyNy4xMjI5MDkxIEwxMjYuMTc5MjY1LDI3LjEyMjkwOTEgTDEyMi40NjM1NTEsMjAuNDM5MjcyNyBMMTE3LjAwNDc3NiwyMC40MzkyNzI3IEwxMTcuMDA0Nzc2LDI3LjEyMjkwOTEgTDExMi45ODIzMjcsMjcuMTIyOTA5MSBaIiBpZD0iRmlsbC0xMCIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgICAgICAgICAgPG1hc2sgaWQ9Im1hc2stNCIgc2tldGNoOm5hbWU9IkNsaXAgMTMiIGZpbGw9IndoaXRlIj4KICAgICAgICAgICAgICAgICAgICA8dXNlIHhsaW5rOmhyZWY9IiNwYXRoLTMiPjwvdXNlPgogICAgICAgICAgICAgICAgPC9tYXNrPgogICAgICAgICAgICAgICAgPGcgaWQ9IkNsaXAtMTMiPjwvZz4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xMzQuOTI4MzY3LDguOTM0NTQ1NDUgTDEzOC45NTA4MTYsOC45MzQ1NDU0NSBMMTM4Ljk1MDgxNiwyNy4xMjM2MzY0IEwxMzQuOTI4MzY3LDI3LjEyMzYzNjQgTDEzNC45MjgzNjcsOC45MzQ1NDU0NSBaIiBpZD0iRmlsbC0xMiIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCIgbWFzaz0idXJsKCNtYXNrLTQpIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTQyLjU4Mjc3Niw4LjkzNDcyNzI3IEwxNTkuOTE3ODc4LDguOTM0NzI3MjcgTDE1OS45MTc4NzgsMTIuMzg3NDU0NSBMMTUzLjI3NDQwOCwxMi4zODc0NTQ1IEwxNTMuMjc0NDA4LDI3LjEyMzgxODIgTDE0OS4yNTE5NTksMjcuMTIzODE4MiBMMTQ5LjI1MTk1OSwxMi4zODc0NTQ1IEwxNDIuNTgyNzc2LDEyLjM4NzQ1NDUgTDE0Mi41ODI3NzYsOC45MzQ3MjcyNyBaIiBpZD0iRmlsbC0xNCIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCIgbWFzaz0idXJsKCNtYXNrLTQpIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTc5LjM4NzQ0OSw4LjkzNDcyNzI3IEwxNzIuMDk5Mjg2LDE4LjgyMiBMMTcyLjA5OTI4NiwyNy4xMjM4MTgyIEwxNjguMDQ3NDQ5LDI3LjEyMzgxODIgTDE2OC4wNDc0NDksMTguODIyIEwxNjAuNzMxNzM1LDguOTM0NzI3MjcgTDE2NS43Mzg2NzMsOC45MzQ3MjcyNyBMMTcwLjIxMjk1OSwxNS4yNTgzNjM2IEwxNzQuNjYxNTMxLDguOTM0NzI3MjcgTDE3OS4zODc0NDksOC45MzQ3MjcyNyBaIiBpZD0iRmlsbC0xNSIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCIgbWFzaz0idXJsKCNtYXNrLTQpIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=',
+ isInsideWell: false,
+ },
+ pageSymbols: {
+ default:
+ 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA5IDEwOSI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJOYW1ubMO2c1/DtnZlcnRvbmluZ18zIiB4MT0iNi40OSIgeTE9IjYyLjU5IiB4Mj0iMzAuMTgiIHkyPSI3MS4yMiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iLjEiIHN0b3AtY29sb3I9IiNkMTViOWIiIHN0b3Atb3BhY2l0eT0iLjEiLz48c3RvcCBvZmZzZXQ9Ii4zNSIgc3RvcC1jb2xvcj0iI2QxNWI5YiIgc3RvcC1vcGFjaXR5PSIuNjgiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNkMTViOWIiIHN0b3Atb3BhY2l0eT0iLjAyIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9Ik5hbW5sw7ZzX8O2dmVydG9uaW5nXzMtMiIgeDE9Ijg2LjE4IiB5MT0iNjcuOTkiIHgyPSI5My42MiIgeTI9IjcwLjciIHhsaW5rOmhyZWY9IiNOYW1ubMO2c1/DtnZlcnRvbmluZ18zIi8+PGxpbmVhckdyYWRpZW50IGlkPSJOYW1ubMO2c1/DtnZlcnRvbmluZ18zLTMiIHgxPSIyNy4xMiIgeTE9IjkiIHgyPSI4NS43NSIgeTI9IjMwLjM0IiB4bGluazpocmVmPSIjTmFtbmzDtnNfw7Z2ZXJ0b25pbmdfMyIvPjwvZGVmcz48cmVjdCB4PSI2Mi4wNCIgeT0iNTguMjMiIHdpZHRoPSIxNi45IiBoZWlnaHQ9IjEwLjMzIiByeD0iMS44OCIgcnk9IjEuODgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzYzNmQ4NyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjIuMTgiLz48cGF0aCBkPSJNNjUuOCw1OC4yM3YtMy43NWMwLTIuNTksMi4xLTQuNjksNC42OS00LjY5czQuNjksMi4xLDQuNjksNC42OXYzLjc1IiBmaWxsPSJub25lIiBzdHJva2U9IiM2MzZkODciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyLjE4Ii8+PGNpcmNsZSBjeD0iNTMuODciIGN5PSIzOC4xNCIgcj0iMTAuMTYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QzNWI5YyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS13aWR0aD0iMi4xOCIvPjxwYXRoIGQ9Ik0zNS43Niw2OS4yYzAtOS4wMSw4LjExLTE2LjMxLDE4LjExLTE2LjMxLDIuMDgsMCw0LjA4LjMyLDUuOTQuOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDM1YjljIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLXdpZHRoPSIyLjE4Ii8+PHJlY3QgeD0iMTQiIHk9IjM1Ljk0IiB3aWR0aD0iNi41NSIgaGVpZ2h0PSI0IiByeD0iLjczIiByeT0iLjczIiBmaWxsPSJub25lIiBzdHJva2U9IiM2NDZlODUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0xNS40NiwzNS45NHYtMS40NmMwLTEsLjgxLTEuODIsMS44Mi0xLjgyczEuODIuODEsMS44MiwxLjgydjEuNDYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzY0NmU4NSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTE3LjI1LDI4LjE1Yy0uNjQuOTUtMi40NywyLjg4LTYuOTksMi43OGwuMDMsNy43N2MuMDIuMTQuNTYsMy43NSw3LjAzLDcuMjIsNi40NS0zLjUyLDYuOTYtNy4xMyw2Ljk4LTcuMjlsLS4wMy03Ljc2Yy00LjUxLjEzLTYuMzctMS43OC03LjAyLTIuNzNaIiBmaWxsPSJub25lIiBzdHJva2U9IiM2NDZlODUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik00NC4yNCw4Ni4xOWMwLTEuNTEsMS4yMi0yLjczLDIuNzMtMi43M2gxMC45MWMxLjUxLDAsMi43MywxLjIyLDIuNzMsMi43M3YxLjgyYzAsMS41MS0xLjIyLDIuNzMtMi43MywyLjczaC0xMC45MWMtMS41MSwwLTIuNzMtMS4yMi0yLjczLTIuNzN2LTEuODJaIiBmaWxsPSJub25lIiBzdHJva2U9IiM2MjZjODciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik01Mi40Myw5OC4wMWgtNS40NmMtMS41MSwwLTIuNzMtMS4yMi0yLjczLTIuNzN2LTEuODJjMC0xLjUxLDEuMjItMi43MywyLjczLTIuNzNoOS41NSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjI2Yzg3IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48Y2lyY2xlIGN4PSI0Ny44OCIgY3k9Ijg3LjUxIiByPSIxLjAxIiBmaWxsPSIjNjI2Yzg3Ii8+PGNpcmNsZSBjeD0iNDcuODgiIGN5PSI5NC41NSIgcj0iMS4wMSIgZmlsbD0iIzYyNmM4NyIvPjxwYXRoIGQ9Ik01NS4zOCw5Ni4zMWw0LjE0LDEuNzJjLjEzLjA1LjI3LjA1LjQsMGw0LjE0LTEuNzJjLjI2LS4xMS4zOS0uNDEuMjgtLjY4LS4wNS0uMTMtLjE1LS4yMi0uMjgtLjI4bC00LjE0LTEuNzJjLS4xMy0uMDUtLjI3LS4wNS0uNCwwbC00LjE0LDEuNzJjLS4yNi4xMS0uMzkuNDEtLjI4LjY4LjA1LjEzLjE1LjIyLjI4LjI4WiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjQ2ZTg1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNNTQuOTcsOTcuNTlsNC41MywxLjg5Yy4xNC4wNi4zLjA2LjQ0LDBsNC41My0xLjg5IiBmaWxsPSJub25lIiBzdHJva2U9IiM2NDZlODUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik01NC45Nyw5OS4wNGw0LjUzLDEuODljLjE0LjA2LjMuMDYuNDQsMGw0LjUzLTEuODkiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzY0NmU4NSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTk2LjgzLDM2LjlsMS45Mi0xLjkyLTEuMzYtMS4zNmMtLjc3LjQ5LTEuOC40LTIuNDgtLjI3cy0uNzYtMS43LS4yNy0yLjQ4bC0xLjM2LTEuMzYtOS4zNyw5LjM3Yy0uMy4zLS4zLjc5LDAsMS4wOWw0LjM5LDQuMzljLjMuMy43OS4zLDEuMDksMGw4LjM0LTguMzQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzYyNmM4NyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PGNpcmNsZSBjeD0iOTIuMjQiIGN5PSIzMy44NCIgcj0iLjM1IiBmaWxsPSIjNjI2Yzg3Ii8+PGNpcmNsZSBjeD0iOTMuMjkiIGN5PSIzNC44OSIgcj0iLjM1IiBmaWxsPSIjNjI2Yzg3Ii8+PGNpcmNsZSBjeD0iOTQuMzMiIGN5PSIzNS45NCIgcj0iLjM1IiBmaWxsPSIjNjI2Yzg3Ii8+PHBhdGggZD0iTTI1LjMzLDgyLjc4Yy04LjYtNy44Ni0xMy45OS0xOS4xNy0xMy45OS0zMS43NCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI05hbW5sw7ZzX8O2dmVydG9uaW5nXzMpIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMS4yMSIvPjxwYXRoIGQ9Ik05Ny4zMiw1Mi44OWMtLjU2LDEzLjExLTYuOTksMjQuNzEtMTYuNzMsMzIuMjIiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNOYW1ubMO2c1/DtnZlcnRvbmluZ18zLTIpIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMS4yMSIvPjxwYXRoIGQ9Ik0yMy4zLDIxLjI3YzcuODMtOC4xNiwxOC44NC0xMy4yNSwzMS4wNS0xMy4yNSwxMy4zNiwwLDI1LjMsNi4wOSwzMy4xOSwxNS42NSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI05hbW5sw7ZzX8O2dmVydG9uaW5nXzMtMykiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIxLjIxIi8+PC9zdmc+',
+ },
+ },
+};
enum Page {
START = 'start',
@@ -56,10 +73,12 @@ export function Previewer() {
};
return (
-
- setCurrentPage(page as Page)} currentPage={currentPage}>
- {renderPreview()}
-
-
+
+
+ setCurrentPage(page as Page)} currentPage={currentPage}>
+ {renderPreview()}
+
+
+
);
}
diff --git a/src/login-web-app/previewer/shared/ui/preview/Preview.tsx b/src/login-web-app/previewer/shared/ui/preview/Preview.tsx
index 62aca9ad..a480e033 100644
--- a/src/login-web-app/previewer/shared/ui/preview/Preview.tsx
+++ b/src/login-web-app/previewer/shared/ui/preview/Preview.tsx
@@ -16,6 +16,11 @@ import { JsonRepresentation } from '../json-representation/JsonRepresentation';
import { Main } from '../main/Main';
import { Header } from '../page-header/PageHeader';
import { PreviewLayout } from '../preview-layout/PreviewLayout';
+import { PageSymbol } from '../../../../src/shared/ui/PageSymbol';
+import { Well } from '../../../../src/haapi-stepper/ui/well/Well';
+import { useHaapiAppConfig } from '../../../../src/shared/feature/app-config/HaapiAppConfigHook';
+import { Logo } from '../../../../src/shared/ui/Logo';
+import styles from './preview.module.css';
interface PreviewProps {
title: string;
@@ -25,6 +30,8 @@ interface PreviewProps {
export function Preview({ title, step, onErrorToggle }: PreviewProps) {
const [, setHasError] = useState(false);
+ const currentPage = step.metadata?.viewName ?? 'Unknown view';
+ const { isInsideWell } = useHaapiAppConfig().theme.logo ?? {};
const handleErrorToggle = (hasError: boolean) => {
setHasError(hasError);
@@ -35,7 +42,14 @@ export function Preview({ title, step, onErrorToggle }: PreviewProps) {
-
+
+ {!isInsideWell &&
}
+
+ {isInsideWell && }
+
+
+
+
diff --git a/src/login-web-app/previewer/shared/ui/preview/preview.module.css b/src/login-web-app/previewer/shared/ui/preview/preview.module.css
new file mode 100644
index 00000000..7b734d03
--- /dev/null
+++ b/src/login-web-app/previewer/shared/ui/preview/preview.module.css
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2026 Curity AB. All rights reserved.
+ *
+ * The contents of this file are the property of Curity AB.
+ * You may not copy or use this file, in either source code
+ * or executable form, except in compliance with terms
+ * set by Curity AB.
+ *
+ * For further information, please contact Curity AB.
+ */
+
+/*
+ * Groups the logo and the well into a single grid item so that the surrounding
+ * `Main` grid always sees exactly two children (this app view + the JSON
+ * representation), regardless of whether the logo renders inside or outside the
+ * well. Mirrors the production layout where the logo stacks above the well.
+ */
+.appView {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--space-3);
+ width: 100%;
+}
diff --git a/src/login-web-app/src/haapi-stepper/README.md b/src/login-web-app/src/haapi-stepper/README.md
index cd5461e8..2710a4be 100644
--- a/src/login-web-app/src/haapi-stepper/README.md
+++ b/src/login-web-app/src/haapi-stepper/README.md
@@ -104,10 +104,10 @@ This is the default behavior and covers the vast majority of deployments (the LW
When the `HaapiStepper` is consumed as a library — e.g. embedded in a third-party app or any context that doesn't set `window.__CONFIG__` — the consumer supplies the bootstrap configuration explicitly via the `config.bootstrap` prop:
```tsx
-import { HaapiStepper } from '@curity/login-web-app/haapi-stepper';
-import type { BootstrapConfiguration } from '@curity/login-web-app/haapi-stepper';
+import { HaapiStepper } from './feature';
+import type { HaapiAppConfig } from '../shared/feature/app-config/types';
-const bootstrapConfig: BootstrapConfiguration = {
+const bootstrapConfig: HaapiAppConfig = {
initialUrl: 'https://idsvr.example.com/oauth/v2/oauth-authorize/...',
haapi: { /* HAAPI web-driver config */ },
theme: { logo: { path: '/logo.svg', isInsideWell: true } },
diff --git a/src/login-web-app/src/shared/feature/app-config/types.ts b/src/login-web-app/src/shared/feature/app-config/types.ts
index 6d1e7d91..3253ebed 100644
--- a/src/login-web-app/src/shared/feature/app-config/types.ts
+++ b/src/login-web-app/src/shared/feature/app-config/types.ts
@@ -6,5 +6,15 @@ export interface HaapiAppConfig extends HaapiStepperBootstrapConfig {
path: string;
isInsideWell: boolean;
};
+ pageSymbols?: PageSymbols;
};
}
+
+export interface PageSymbols {
+ /** Map of full HAAPI viewName -> symbol path. Highest precedence. */
+ views?: Record;
+ /** Map of plugin implementation type (e.g. `html-form`, `bankid`) -> symbol path. */
+ plugins?: Record;
+ /** Fallback symbol path used when no per-view / per-plugin entry matches. */
+ default?: string;
+}
diff --git a/src/login-web-app/src/shared/ui/Layout.spec.tsx b/src/login-web-app/src/shared/ui/Layout.spec.tsx
index aed78de9..fcc43fac 100644
--- a/src/login-web-app/src/shared/ui/Layout.spec.tsx
+++ b/src/login-web-app/src/shared/ui/Layout.spec.tsx
@@ -9,73 +9,116 @@
* For further information, please contact Curity AB.
*/
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
import { Layout } from './Layout';
+import { HaapiStepperContext } from '../../haapi-stepper/feature/stepper/HaapiStepperContext';
+import type { PageSymbols } from '../feature/app-config/types';
+import type { HaapiStepperAPI, HaapiStepperStep } from '../../haapi-stepper/feature/stepper/haapi-stepper.types';
+import type { HaapiAppConfig } from '../feature/app-config/types';
import { HaapiAppConfigContext } from '../feature/app-config/HaapiAppConfigContext';
-import { HaapiAppConfig } from '../feature/app-config/types';
-const renderLayout = (isInsideWell: boolean) => {
- const config: HaapiAppConfig = {
- initialUrl: 'https://example/start',
- haapi: {} as HaapiAppConfig['haapi'],
- theme: { logo: { path: '/assets/logo.svg', isInsideWell } },
- };
- return render(
-
-
-
-
-
- );
-};
+describe('Layout', () => {
+ describe('Logo placement', () => {
+ it('renders the logo inside the well when theme.logo.isInsideWell is true', () => {
+ const { container } = renderLayout({ isInsideWell: true });
+ const well = container.querySelector('.haapi-stepper-well');
+ const logo = container.querySelector('img.haapi-stepper-logo');
+
+ expect(well).not.toBeNull();
+ expect(logo).not.toBeNull();
+ expect(well?.contains(logo)).toBe(true);
+ });
+
+ it('renders the logo before the well (not inside it) when theme.logo.isInsideWell is false', () => {
+ const { container } = renderLayout({ isInsideWell: false });
+ const main = container.querySelector('main.app-layout');
+ const well = container.querySelector('.haapi-stepper-well');
+ const logo = container.querySelector('img.haapi-stepper-logo');
+
+ expect(main).not.toBeNull();
+ expect(well).not.toBeNull();
+ expect(logo).not.toBeNull();
-describe('Layout — logo placement', () => {
- it('renders the logo inside the well when theme.logo.isInsideWell is true', () => {
- const { container } = renderLayout(true);
- const well = container.querySelector('.haapi-stepper-well');
- const logo = container.querySelector('img.haapi-stepper-logo');
+ // Logo is a sibling of the well, not a descendant.
+ expect(well?.contains(logo)).toBe(false);
+ expect(main?.contains(logo)).toBe(true);
- expect(well).not.toBeNull();
- expect(logo).not.toBeNull();
- expect(well?.contains(logo)).toBe(true);
+ // Logo appears before the well in document order.
+ expect(logo!.compareDocumentPosition(well!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ });
+
+ it('renders children and the well but no logo element when theme.logo is not configured', () => {
+ const { container, getByTestId } = renderLayout({ withoutLogo: true });
+ expect(getByTestId('content')).toBeInTheDocument();
+ expect(container.querySelector('.haapi-stepper-well')).not.toBeNull();
+ expect(container.querySelector('img.haapi-stepper-logo')).toBeNull();
+ });
});
- it('renders the logo before the well (not inside it) when theme.logo.isInsideWell is false', () => {
- const { container } = renderLayout(false);
- const main = container.querySelector('main.app-layout');
- const well = container.querySelector('.haapi-stepper-well');
- const logo = container.querySelector('img.haapi-stepper-logo');
+ describe('Page symbol', () => {
+ it('renders the resolved page symbol above the children for the current step', () => {
+ const { container } = renderLayout({
+ pageSymbols: { plugins: { 'html-form': '/symbols/html-form.svg' } },
+ currentStep: stepWithViewName('authenticator/html-form/index'),
+ });
+
+ const pageSymbol = container.querySelector('img.haapi-stepper-page-symbol-image');
+ const content = container.querySelector('[data-testid="content"]');
- expect(main).not.toBeNull();
- expect(well).not.toBeNull();
- expect(logo).not.toBeNull();
+ expect(pageSymbol).not.toBeNull();
+ expect(pageSymbol).toHaveAttribute('src', '/symbols/html-form.svg');
+ // Page symbol appears before the children in document order.
+ expect(pageSymbol!.compareDocumentPosition(content!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ });
- // Logo is a sibling of the well, not a descendant.
- expect(well?.contains(logo)).toBe(false);
- expect(main?.contains(logo)).toBe(true);
+ it('renders nothing for the page symbol when theme.pageSymbols is absent', () => {
+ const { container } = renderLayout({
+ currentStep: stepWithViewName('authenticator/html-form/index'),
+ });
- // Logo appears before the well in document order.
- expect(logo!.compareDocumentPosition(well!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ expect(container.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull();
+ });
});
+});
- it('renders children and the well but no logo element when theme.logo is not configured', () => {
- const configWithoutLogo: HaapiAppConfig = {
- initialUrl: 'https://example/start',
- haapi: {} as HaapiAppConfig['haapi'],
- theme: {},
- };
+const emptyStepperAPI: HaapiStepperAPI = {
+ loading: false,
+ history: [],
+ nextStep: vi.fn(),
+ currentStep: null,
+ error: null,
+};
+
+const stepWithViewName = (viewName: string): HaapiStepperStep =>
+ ({ metadata: { templateArea: 'lwa', viewName } }) as unknown as HaapiStepperStep;
- const { container, getByTestId } = render(
-
+interface LayoutHarnessOptions {
+ isInsideWell?: boolean;
+ pageSymbols?: PageSymbols;
+ currentStep?: HaapiStepperStep | null;
+ withoutLogo?: boolean;
+}
+
+const renderLayout = ({
+ isInsideWell = false,
+ pageSymbols,
+ currentStep = null,
+ withoutLogo = false,
+}: LayoutHarnessOptions = {}) => {
+ const config: HaapiAppConfig = {
+ initialUrl: 'https://example/start',
+ haapi: {} as HaapiAppConfig['haapi'],
+ theme: { logo: withoutLogo ? undefined : { path: '/assets/logo.svg', isInsideWell }, pageSymbols },
+ };
+ const stepper: HaapiStepperAPI = { ...emptyStepperAPI, currentStep };
+ return render(
+
+
-
- );
-
- expect(getByTestId('content')).toBeInTheDocument();
- expect(container.querySelector('.haapi-stepper-well')).not.toBeNull();
- expect(container.querySelector('img.haapi-stepper-logo')).toBeNull();
- });
-});
+
+
+ );
+};
diff --git a/src/login-web-app/src/shared/ui/Layout.tsx b/src/login-web-app/src/shared/ui/Layout.tsx
index a1388075..693cfa8c 100644
--- a/src/login-web-app/src/shared/ui/Layout.tsx
+++ b/src/login-web-app/src/shared/ui/Layout.tsx
@@ -13,8 +13,11 @@ import { ReactNode } from 'react';
import { Well } from '../../haapi-stepper/ui/well/Well';
import { useHaapiAppConfig } from '../feature/app-config/HaapiAppConfigHook';
import { Logo } from './Logo';
+import { PageSymbol } from './PageSymbol';
+import { useHaapiStepper } from '../../haapi-stepper/feature';
export const Layout = ({ children }: { children: ReactNode }) => {
+ const { currentStep } = useHaapiStepper();
const { isInsideWell } = useHaapiAppConfig().theme.logo ?? {};
return (
@@ -23,7 +26,10 @@ export const Layout = ({ children }: { children: ReactNode }) => {
{!isInsideWell && }
{isInsideWell && }
- {children}
+
>
diff --git a/src/login-web-app/src/shared/ui/PageSymbol.spec.tsx b/src/login-web-app/src/shared/ui/PageSymbol.spec.tsx
new file mode 100644
index 00000000..5dd1757e
--- /dev/null
+++ b/src/login-web-app/src/shared/ui/PageSymbol.spec.tsx
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2026 Curity AB. All rights reserved.
+ *
+ * The contents of this file are the property of Curity AB.
+ * You may not copy or use this file, in either source code
+ * or executable form, except in compliance with terms
+ * set by Curity AB.
+ *
+ * For further information, please contact Curity AB.
+ */
+
+import { describe, expect, it } from 'vitest';
+import { render } from '@testing-library/react';
+import { PageSymbol } from './PageSymbol';
+import { HaapiAppConfigContext } from '../feature/app-config/HaapiAppConfigContext';
+import type { HaapiAppConfig, PageSymbols } from '../feature/app-config/types';
+
+const buildConfig = (pageSymbols?: PageSymbols): HaapiAppConfig => ({
+ initialUrl: 'https://example/start',
+ haapi: {} as HaapiAppConfig['haapi'],
+ theme: {
+ logo: { path: '/assets/logo.svg', isInsideWell: false },
+ pageSymbols,
+ },
+});
+
+const renderPageSymbol = (viewName: string | undefined, pageSymbols?: PageSymbols) =>
+ render(
+
+
+
+ );
+
+describe('PageSymbol', () => {
+ const pageSymbols: PageSymbols = {
+ views: {
+ 'authenticator/html-form/create-account/post': '/symbols/create-account.svg',
+ 'authentication-action/email-verifier/confirm': '/symbols/email-verifier-confirm.svg',
+ 'consentor/scope-consent/review': '/symbols/scope-consent-review.svg',
+ },
+ plugins: {
+ 'html-form': '/symbols/html-form.svg',
+ 'email-verifier': '/symbols/email-verifier.svg',
+ 'scope-consent': '/symbols/scope-consent.svg',
+ },
+ default: '/symbols/default.svg',
+ };
+
+ it.each([
+ ['authenticator', 'authenticator/html-form/create-account/post', '/symbols/create-account.svg'],
+ ['authentication-action', 'authentication-action/email-verifier/confirm', '/symbols/email-verifier-confirm.svg'],
+ ['consentor', 'consentor/scope-consent/review', '/symbols/scope-consent-review.svg'],
+ ])(
+ 'renders the exact `views` entry for the %s category even when a plugin or default would also match',
+ (_, viewName, expected) => {
+ renderPageSymbol(viewName, pageSymbols);
+ expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toHaveAttribute(
+ 'src',
+ expected
+ );
+ }
+ );
+
+ it.each([
+ ['authenticator', 'authenticator/html-form/index', '/symbols/html-form.svg'],
+ ['authentication-action', 'authentication-action/email-verifier/verify', '/symbols/email-verifier.svg'],
+ ['consentor', 'consentor/scope-consent/consent', '/symbols/scope-consent.svg'],
+ ])(
+ 'falls back to the plugin-type entry for the %s category when no `views` entry matches',
+ (_, viewName, expected) => {
+ renderPageSymbol(viewName, pageSymbols);
+ expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toHaveAttribute(
+ 'src',
+ expected
+ );
+ }
+ );
+
+ it('falls back to `default` when neither `views` nor `plugins` matches', () => {
+ renderPageSymbol('authenticator/unknown-plugin/index', pageSymbols);
+ expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toHaveAttribute(
+ 'src',
+ '/symbols/default.svg'
+ );
+ });
+
+ it('falls back to `default` when the viewName is outside the three plugin categories', () => {
+ renderPageSymbol('views/select-authenticator/index', pageSymbols);
+ expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toHaveAttribute(
+ 'src',
+ '/symbols/default.svg'
+ );
+ });
+
+ it('renders nothing when nothing resolves and no `default` is configured', () => {
+ renderPageSymbol('authenticator/unknown-plugin/index', {
+ plugins: { 'html-form': '/symbols/html-form.svg' },
+ });
+ expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull();
+ });
+
+ it('renders nothing when pageSymbols is absent', () => {
+ renderPageSymbol('authenticator/html-form/index', undefined);
+ expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull();
+ });
+
+ it('renders nothing when pageSymbols is empty', () => {
+ renderPageSymbol('authenticator/html-form/index', {});
+ expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull();
+ });
+
+ it('renders nothing when viewName is undefined', () => {
+ renderPageSymbol(undefined, pageSymbols);
+ expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull();
+ });
+
+ it('renders nothing when viewName is an empty string', () => {
+ renderPageSymbol('', pageSymbols);
+ expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull();
+ });
+});
diff --git a/src/login-web-app/src/shared/ui/PageSymbol.tsx b/src/login-web-app/src/shared/ui/PageSymbol.tsx
new file mode 100644
index 00000000..222015d3
--- /dev/null
+++ b/src/login-web-app/src/shared/ui/PageSymbol.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2026 Curity AB. All rights reserved.
+ *
+ * The contents of this file are the property of Curity AB.
+ * You may not copy or use this file, in either source code
+ * or executable form, except in compliance with terms
+ * set by Curity AB.
+ *
+ * For further information, please contact Curity AB.
+ */
+
+import { useHaapiAppConfig } from '../feature/app-config/HaapiAppConfigHook';
+import { resolvePageSymbol } from '../util/resolve-page-symbol';
+
+interface PageSymbolProps {
+ viewName: string | undefined;
+}
+
+/**
+ * Renders the page symbol icon associated with the current step's HAAPI `viewName`.
+ *
+ * The mapping comes from `theme.pageSymbols` in the bootstrap configuration and is resolved by
+ * {@link resolvePageSymbol}. When `theme.pageSymbols` is absent, when `viewName` is absent, or when
+ * no entry resolves, this component renders nothing.
+ */
+export const PageSymbol = ({ viewName }: PageSymbolProps) => {
+ const { theme } = useHaapiAppConfig();
+ const src = resolvePageSymbol(viewName, theme.pageSymbols);
+
+ if (!src) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/src/login-web-app/src/shared/util/css/styles.css b/src/login-web-app/src/shared/util/css/styles.css
index feea940e..7cb364d4 100644
--- a/src/login-web-app/src/shared/util/css/styles.css
+++ b/src/login-web-app/src/shared/util/css/styles.css
@@ -248,6 +248,25 @@ svg {
margin-block: 0 var(--space-2);
}
+.haapi-stepper-page-symbol {
+ text-align: center;
+ width: var(--login-symbol-size);
+ height: var(--login-symbol-size);
+ display: var(--login-symbol-display);
+ margin: 0 auto;
+ justify-content: center;
+ align-items: center;
+ margin-top: var(--login-symbol-margin-top);
+ margin-bottom: var(--login-symbol-margin-bottom);
+ border-radius: var(--login-symbol-border-radius);
+}
+
+.haapi-stepper-page-symbol-image {
+ width: var(--login-symbol-size);
+ height: var(--login-symbol-size);
+ object-fit: contain;
+}
+
.haapi-stepper-links {
@extend .center, .py3, .flex, .flex-column;
}
diff --git a/src/login-web-app/src/shared/util/resolve-page-symbol.ts b/src/login-web-app/src/shared/util/resolve-page-symbol.ts
new file mode 100644
index 00000000..353874cf
--- /dev/null
+++ b/src/login-web-app/src/shared/util/resolve-page-symbol.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2026 Curity AB. All rights reserved.
+ *
+ * The contents of this file are the property of Curity AB.
+ * You may not copy or use this file, in either source code
+ * or executable form, except in compliance with terms
+ * set by Curity AB.
+ *
+ * For further information, please contact Curity AB.
+ */
+
+import type { PageSymbols } from '../feature/app-config/types';
+
+/**
+ * Resolves the page symbol image path for a given HAAPI step `viewName` against the
+ * `theme.pageSymbols` configuration delivered by the server bootstrap.
+ *
+ * Resolution order:
+ * 1. Exact match in `pageSymbols.views`.
+ * 2. Plugin-type match (extracted from `viewName` via {@link PLUGIN_TYPE_FROM_VIEW_NAME}) in `pageSymbols.plugins`.
+ * 3. `pageSymbols.default`.
+ * 4. `undefined` when no rule resolves — callers should render nothing.
+ *
+ * Returns `undefined` for any falsy input (no `viewName`, no `pageSymbols`).
+ */
+export const resolvePageSymbol = (
+ viewName: string | undefined,
+ pageSymbols: PageSymbols | undefined
+): string | undefined => {
+ /**
+ * Extracts the plugin implementation type from a HAAPI `viewName` of the form
+ * `//`, where category is `authenticator`, `authentication-action`
+ * or `consentor`.
+ */
+ const PLUGIN_TYPE_FROM_VIEW_NAME = /^(?:authenticator|authentication-action|consentor)\/([^/]+)\/.*/;
+
+ if (!viewName || !pageSymbols) {
+ return undefined;
+ }
+
+ const exactMatch = pageSymbols.views?.[viewName];
+ if (exactMatch) {
+ return exactMatch;
+ }
+
+ const pluginType = PLUGIN_TYPE_FROM_VIEW_NAME.exec(viewName)?.[1];
+ const pluginMatch = pluginType ? pageSymbols.plugins?.[pluginType] : undefined;
+ if (pluginMatch) {
+ return pluginMatch;
+ }
+
+ return pageSymbols.default;
+};