diff --git a/.github/workflows/lwa-github-ci-workflow.yml b/.github/workflows/lwa-github-ci-workflow.yml index 7592f42d..40d31b33 100644 --- a/.github/workflows/lwa-github-ci-workflow.yml +++ b/.github/workflows/lwa-github-ci-workflow.yml @@ -33,16 +33,16 @@ jobs: run: npm ci - name: SDK — Check Formatting - run: npm run prettier-check -w src/haapi-react-sdk + run: npm run prettier-check -w @curity/haapi-react-sdk - name: SDK — Lint - run: npm run lint -w src/haapi-react-sdk + run: npm run lint -w @curity/haapi-react-sdk - name: SDK — Typecheck - run: npm run typecheck -w src/haapi-react-sdk + run: npm run typecheck -w @curity/haapi-react-sdk - name: SDK — Run Tests - run: npm run test -w src/haapi-react-sdk -- run + run: npm run test -w @curity/haapi-react-sdk -- run - name: App — Check Formatting run: npm run prettier-check -w src/haapi-react-app diff --git a/index.html b/index.html index d93c2a37..01856580 100644 --- a/index.html +++ b/index.html @@ -324,6 +324,14 @@ width: fit-content; } + .documentation { + margin-top: 1.5rem; + width: 100%; + text-align: center; + border-top: 1px solid #eee; + padding-top: 1rem; + } + .status { margin-top: auto; padding-top: 1rem; @@ -525,12 +533,23 @@

HAAPI React App

Open previewer http://localhost:5174/ +
+ + Read HAAPI React SDK documentation + +

Useful Commands

npm install npm run start:haapi-react-app npm run build:haapi-react-app
+
=6.9.0" @@ -365,13 +365,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -381,13 +381,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -397,36 +397,36 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -436,36 +436,36 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -485,12 +485,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -500,13 +500,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -555,31 +555,31 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -587,13 +587,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -3849,6 +3849,21 @@ "node": ">=18" } }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7186,9 +7201,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", - "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==", + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -7394,9 +7409,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", "funding": [ { "type": "opencollective", @@ -7413,11 +7428,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -7600,9 +7615,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001782", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz", - "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==", + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "funding": [ { "type": "opencollective", @@ -9012,9 +9027,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.328", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz", - "integrity": "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==", + "version": "1.5.378", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", + "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -14955,10 +14970,13 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "license": "MIT" + "version": "2.0.49", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.49.tgz", + "integrity": "sha512-f06bl1D+8ZDkn2oOQQKAh5/otFWqVnM1Q5oerA8Pex7UfT66Tx4IPHIqVVFKqFT3FUtaDstdgkM7yT7JWhqxfw==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-package-data": { "version": "2.5.0", @@ -16203,21 +16221,42 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -16231,6 +16270,16 @@ "node": ">=0.10.0" } }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", @@ -16862,12 +16911,13 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, diff --git a/package.json b/package.json index 422c8764..81a682a0 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "start:component-library": "npm run -w src/common/component-library watch", "start:identity-server": "npm run -w src/identity-server start", "build:identity-server": "npm run -w src/identity-server build", - "start:haapi-react-app": "npm run -w src/haapi-react-app start", + "start:haapi-react-app": "npm run -w src/haapi-react-app previewer:serve", "build:haapi-react-app": "npm run -w src/haapi-react-app build", "build:ssp": "npm run -w src/self-service-portal/app build", "build:css": "npm run -w src/common/css build", @@ -49,4 +49,4 @@ "concurrently": "^9.1.2", "cross-env": "^7.0.3" } -} \ No newline at end of file +} diff --git a/src/haapi-react-app/README.md b/src/haapi-react-app/README.md index 495c0c6b..bf02bec1 100644 --- a/src/haapi-react-app/README.md +++ b/src/haapi-react-app/README.md @@ -2,7 +2,11 @@ ## Project & tools -Install packages with `npm install`. +Install all workspaces from the repo root: + +```shell +npm i --prefix "$(git rev-parse --show-toplevel)" +``` - React - Built with Vite @@ -13,14 +17,32 @@ Install packages with `npm install`. - IDEs usually have Prettier integrated with the "format code" commands. - IntelliJ has a [plugin](https://plugins.jetbrains.com/plugin/10456-prettier). Additional settings in `Languages & Frameworks | JavaScript | Prettier`. +## SDK documentation + +This app is built on the [HAAPI React SDK](../haapi-react-sdk/README.md). + +> 📖 **[Read the HAAPI React SDK docs online](https://curity.io/docs/identity-server/developer-guide/haapi-sdks/web-sdk)** +> (Curity Docs → Developer Guide → HAAPI SDKs → Web SDK). The site is generated from the SDK package — +> see the [SDK README](../haapi-react-sdk/README.md) for how the documentation works. + +## Previewer + +A standalone playground for browsing the SDK's UI against mocked HAAPI steps — pick an example to see how the +components render every authentication step, without a running Identity Server. Run it from this directory: + +```shell +npm run previewer +``` + ## Development setup The Vite development server is used both to serve the application and as a proxy for specific Identity Server endpoints. This allows running the application as a type-0 client (which is the realistic scenario) and still get all the Vite/React development facilities. Follow these steps to get started: + 1. Start Identity Server locally. - - The server should have a minimum setup to allow running OAuth authorization flows with user interaction. + - The server should have a minimum setup to allow running OAuth authorization flows with user interaction. 2. In this directory, run `IDSVR_HOME= ./configure-idsvr-dev.sh`. - `IDSVR_HOME` should contain the path to the home directory of the running Identity Server instance. 3. Run `npm run dev`. @@ -37,9 +59,11 @@ To test the updates, enable the API-driven UI in the Identity Server instance an ## Error Handling ### ErrorBoundary + **Purpose**: Global error boundary that catches unhandled React errors and displays fallback UI. **Example Usage**: + ```tsx @@ -49,47 +73,53 @@ To test the updates, enable the API-driven UI in the Identity Server instance an ``` **Features:** + - Catches JavaScript errors anywhere in the component tree - Displays user-friendly error messages with retry functionality - Prevents entire application crashes - Includes error reporting for debugging - ## Folder Structure Inspired by the Domain-Driven Design approach, this project's folder structure is organized by 2 dimensions/levels: ### **Dimension 1: Subdomain/Feature** + - **First level folders** represent different **subdomains or features** the app implements - Each subdomain encapsulates related functionality #### **Shared Code Organization** + - **`shared/` folder**: Contains libraries used by multiple subdomains/features ### **Dimension 2: Technical Layer Types** + Within each subdomain, code is organized by these technical layer types: #### **`feature/`** + - **Purpose**: Smart UI components with data access - **Contains**: Business logic components that connect to data sources. "Smart" components that manage state and side effects - **Examples**: Components that handle authentication flows, form submissions, API calls #### **`ui/`** + - **Purpose**: Presentational components only - **Contains**: "Dumb" components focused on rendering. No business logic, just UI rendering based on props - **Examples**: Buttons, input fields, layout components, styled elements #### **`data-access/`** + - **Purpose**: Backend interaction and state management - **Contains**: API clients, state management code, data transformation. Handles all external data operations - **Examples**: HTTP service functions, Redux stores #### **`util/`** + - **Purpose**: Low-level shared utilities - **Contains**: Helper functions, constants, type definitions. Reusable across multiple components/features - **Examples**: Date formatters, validation functions, common types - ### **Example Schema** ``` @@ -106,6 +136,7 @@ src/ ``` This structure promotes: + - **Discoverability**: Clear separation makes code easy to find - **Maintainability**: Similar responsibilities are grouped together - **Reusability**: Shared components can be easily identified and reused diff --git a/src/haapi-react-app/package.json b/src/haapi-react-app/package.json index 36d95c1f..101fb2e0 100644 --- a/src/haapi-react-app/package.json +++ b/src/haapi-react-app/package.json @@ -25,6 +25,7 @@ "lint": "eslint .", "prettier-check": "prettier --check .", "previewer": "vite serve previewer --open", + "previewer:serve": "vite serve previewer", "watch:css-lib": "npm run watch --workspace @curity/ui-kit-css-lib" }, "dependencies": { diff --git a/src/haapi-react-sdk/.prettierignore b/src/haapi-react-sdk/.prettierignore index 47663139..b7892598 100644 --- a/src/haapi-react-sdk/.prettierignore +++ b/src/haapi-react-sdk/.prettierignore @@ -1,3 +1,6 @@ .prettierrc tsconfig.* README.md + +# The docs are their own workspace with its own prettier-check (run via `-w src/haapi-react-sdk`). +docs diff --git a/src/haapi-react-sdk/README.md b/src/haapi-react-sdk/README.md new file mode 100644 index 00000000..ffe3d8fa --- /dev/null +++ b/src/haapi-react-sdk/README.md @@ -0,0 +1,353 @@ +# Overview + +The HAAPI React SDK is a set of React components that fully manages HAAPI authentication +flows in the frontend. It works out of the box with minimal setup and lets you customize the UI only as far as you need. + +[Browse the HAAPI step catalog](./docs/sections/00-overview/HaapiStepperPreviewHaapiReactSDKPlaygroundExample.tsx) + + + +## Documentation + +The docs website is generated **from this package**. Edit the files here and the documentation website follows. + +> 📖 **[Read the docs online](https://curity.io/docs/identity-server/developer-guide/haapi-sdks/web-sdk)** +> (Curity Docs → Developer Guide → HAAPI SDKs → Web SDK). +> The generator lives in the `identity-server` repo, at `docs/haapi-react-sdk-docgen` — its README +> explains how it works and how to preview the site locally. + +### One convention for the whole tree + +The `docs/sections/` tree **is** the site's menu. At every level, a `NN-name` folder is a menu entry: +- the number is its position +- the name is its title and URL (readers never see the prefix) +- its `README.md` declares its description and the **ordered** list of the library's exports documented in + the section, as a plain numbered list (page order = list order; child folders follow, by prefix). +- a folder named like one of the exports documents that export: `01-form-ui/` is `HaapiStepperFormUI`'s + own page, and its list documents the component's parts. + +Renumber or rename a folder and the site follows. `_`-prefixed folders (the harness) are never menu entries. + +### What becomes what + +Sections differ only in where their page **content** comes from: + +| You edit… | The website shows… | +|---|---| +| This README | The **Overview** pages — one page per `##` section. | +| TSDoc comments on the library's exports | The **API Reference** section — one page per export listed in a section README's numbered list. Every public export must be listed somewhere, or tagged `@docsIgnore`, or generation fails. | +| `.tsx` files in `docs/` | The code examples — Live, editable **playgrounds**. | + +### How an example shows up on the site + +An example is one `.tsx` file in `docs/`. It can appear in three places: + +- **On an Overview page** — add a markdown link to the file from this README. On GitHub it stays a normal link; on the + website it becomes a playground, right where the link is. +- **On an API Reference page** — put `{@see_example ./docs/…/.tsx}` in that + export's TSDoc comment. The playground appears where you put the marker. +- **In the Examples menu** — every `.tsx` in the examples section gets its own page, in its folder's + place in the menu (the one convention above). + +Every example lives in the folder of the section that shows it: `docs/sections/00-overview/` (embedded via a README +link), `docs/sections/01-api-reference/` (embedded via a `{@see_example}` marker), or `docs/sections/02-examples/…` (its own +page in the menu). + +The `_harness/` folder is different: it holds the helpers the playgrounds need (`ExamplePreviewer`, +the step catalog, …). Nothing in it ever becomes a page. + +### Adding an example, step by step + +1. Create the file in the folder of the section that will show it: `docs/sections/00-overview/` (embedded on an + Overview page), `docs/sections/01-api-reference/` (embedded on an API Reference page), or the menu-group folder + (e.g. `docs/sections/02-examples/01-render-interceptors/`) for its own page in the Examples menu. +2. Name it ending in `HaapiReactSDKPlaygroundExample` — for example + `StepLinksHaapiReactSDKPlaygroundExample.tsx`. The file name (minus `.tsx`) is the example's id. +3. Write a small app with a `default` `App` export, usually wrapped in ``. Every + example is typechecked, linted and smoke-rendered (`docs/DocsExamples.spec.tsx`) by this repo's CI. +4. Link it from this README or add a `{@see_example}` marker to the TSDoc of the component (see above). If you placed it under `examples/`, it gets its own page in the Examples menu automatically. +5. Re-run the generator to see the result. + + + +## Previewer + +Browse the default UI for every HAAPI authentication step: pick a step to see how `HaapiStepperStepUI` +renders it out of the box, then edit the code to see your changes live. + +[Browse the HAAPI step catalog](./docs/sections/00-overview/HaapiStepperPreviewHaapiReactSDKPlaygroundExample.tsx) + +## Glossary + +- **Flow**: sequence of steps that results in either a successful authentication (`HAAPI_STEPS.COMPLETED_WITH_SUCCESS`) or an error/failure (`HAAPI_PROBLEM_STEPS.COMPLETED_WITH_ERROR`). +- **Step**: A single stage in the authentication flow, often represented as a screen (e.g., a login page). A step can be composed of actions, links, and messages. + - [Step types](./haapi-stepper/data-access/types/haapi-step.types.ts) +- **Action**: instructions about how to progress to the next step in the authentication flow. Actions often require specific user input and change the state of the authentication (e.g., submitting a form). There are three kinds of action: **form** (e.g. a username/password login form), **client operation** (e.g. a BankID or WebAuthn operation) and **selector** (e.g. choosing an authenticator). + - [Action types](./haapi-stepper/data-access/types/haapi-action.types.ts) +- **Link**: instructions about how to navigate to an alternative but related path (e.g. starting a password reset flow from the main authentication step) + - [Link](./haapi-stepper/data-access/types/haapi-step.types.ts#L335) +- **Message**: Text that provides context to the user about the state of the authentication flow and possible interaction options (e.g., validation errors, warnings, or instructions). + - [Message](./haapi-stepper/data-access/types/haapi-step.types.ts#L324) + +Check out the following HAAPI documentation for in-depth technical details: + +* [Browserless Login Solution](https://curity.io/product/user-journey-orchestration/browserless-login/) +* [What is Hypermedia Authentication API](https://curity.io/resources/learn/what-is-hypermedia-authentication-api/) +* [HAAPI Data Model](https://curity.io/docs/haapi-data-model/latest/). + + +## Main actors + +`HaapiStepper` **runs the flow**; everything else is about **how you render it**. + +### `HaapiStepper` — runs the flow + +A headless provider that manages multi-step HAAPI authentication workflows. Wrap your app in it. + +```tsx + + {/* your UI goes here */} + +``` + +[Read the HaapiStepper reference](/api-reference/stepper) + +### `useHaapiStepper()` — read & advance the flow + +A hook that exposes the ongoing `HaapiStepper` authentication flow: its current step and state +(`currentStep`, `loading`, `error`), the `history` of steps taken so far, and a `nextStep` +function to advance it. + +```tsx +const { currentStep, loading, error, history, nextStep } = useHaapiStepper(); +``` + +[Read the useHaapiStepper reference](/api-reference/use-haapi-stepper) + +### `HaapiStepperStepUI` — the default UI + +Renders any HAAPI flow step out of the box, providing a complete default **opinionated** login UI. It is the fastest way to get HAAPI flows running, and the starting point you customize from. + +```tsx + + + +``` + +[See example](./docs/sections/00-overview/DefaultRenderingHaapiReactSDKPlaygroundExample.tsx) + +[Read the HaapiStepperStepUI reference](/api-reference/step-ui) + +### HAAPI stepper UI components — the building blocks + +The UI representation of the HAAPI entities (`HaapiStep` → `HaapiStepperStepUI`, `HaapiUserMessage` → `HaapiStepperMessageUI`…). These are the building blocks `HaapiStepperStepUI` is made of, and what you compose your own UI from. + +```tsx +function Step() { + const { currentStep, nextStep } = useHaapiStepper(); + if (!currentStep) return null; + + const { actions, messages, links } = currentStep.dataHelpers; + + return ( + <> + + + + + ); +} + + + + +``` + +[See example](./docs/sections/00-overview/StepBuildingBlocksHaapiReactSDKPlaygroundExample.tsx) + +[Read the UI components reference](/api-reference/ui-components) + +## Customization + +Start with the zero-effort default and adopt customization **only as far as you need**. + +| | Effort | Control | What you use | Best for | +|---|--------|---------|--------------|----------| +| Default | None | Low | `HaapiStepper` + `HaapiStepperStepUI` | getting HAAPI flows running out of the box | +| Styles customization | Very low | Look only | CSS classes (`.haapi-stepper-*`) | restyling the default UI | +| Render interceptors | Low | Medium | `HaapiStepperStepUI` + interceptor props | tweaking the default UI | +| UI composition | High | Full | `HaapiStepper` + `useHaapiStepper` hook + UI components | custom layout, grouping, complex/behaviour | +| Mixed | Mixed | Full | a combination of the above | the default UI with localized custom parts | + +### Default — works from scratch + +Renders the complete HAAPI flow UI. + +```tsx + + + +``` + +[See example](./docs/sections/00-overview/DefaultRenderingHaapiReactSDKPlaygroundExample.tsx) + +### Styles customization + +The UI components emit plain `.haapi-stepper-*` CSS class names — restyle the default UI just by +overriding those classes in your own stylesheet, no code changes needed. + +```css +.haapi-stepper-button { + background: #6200ee; + border-radius: 8px; +} +``` + +[Restyle the button with CSS](./docs/sections/00-overview/StylesButtonCustomizationHaapiReactSDKPlaygroundExample.tsx) + +### CSS customization + +The HAAPI UI components are styled via plain CSS class names — no CSS-in-JS, no inline styles. The components only emit class names; the actual rules live in a stylesheet shipped alongside the host application's global stylesheet. For example, in the case of the `haapi-react-app`, in `haapi-react-app/src/shared/util/css/styles.css`. + +#### Importing CSS styles + +Import the stylesheet once from the consuming application's entry point (e.g. `main.tsx`): + +```ts +import './shared/util/css/styles.css'; +``` + +By default, the rules in `styles.css` compose utility classes from [Curity CSS Library](https://github.com/curityio/ui-kit/tree/main/src/common/css) (imported at the top of the file) using PostCSS `@extend` — e.g. `.haapi-stepper-button { @extend .button, .button-medium, .button-primary, .w100, .mt2; }`. The components themselves only know about the `.haapi-stepper-*` class names, so consumers are free to back those classes with anything they like. + +#### Overriding or extending the defaults + +Because the components emit static class names, consumers can: + +- **Override / Extend**: define rules for the same class names — or append additional CSS — in a separate stylesheet imported after `styles.css`. +- **Replace**: skip the default import entirely and provide your own definitions for the classes listed below — written in plain CSS, or composed from any third-party library, for example Tailwind CSS. + +The Curity utility composition shown above is just how *this* project chose to implement the defaults; it is not a contract. Nothing in the components requires `@curity/ui-kit-css`, PostCSS, or `@extend`. + +
+Available CSS classes + +| Class | Used by | Purpose | +|-------|---------|---------| +| `.haapi-stepper-selector` | `HaapiStepperSelectorUI` | Selector action container | +| `.haapi-stepper-authenticator-button` | `HaapiStepperFormSubmitButton` | Authenticator-selector option button (applied automatically when the action carries `authenticatorType`); combine with `.button-` (e.g. `.button-google`) to get the per-authenticator icon color | +| `.haapi-stepper-messages` | `HaapiStepperMessagesUI` | Messages container | +| `.haapi-stepper-form-field-text-input` | `HaapiStepperTextFormFieldUI` | Text input fields | +| `.haapi-stepper-form-field-text-label` | `HaapiStepperTextFormFieldUI` | Form field labels | +| `.haapi-stepper-form-field-checkbox-input` | `HaapiStepperCheckboxFormFieldUI` | Checkbox inputs | +| `.haapi-stepper-form-field-checkbox-label` | `HaapiStepperCheckboxFormFieldUI` | Checkbox-specific labels | +| `.haapi-stepper-form-field-select-input` | `HaapiStepperSelectFormFieldUI` | Select inputs | +| `.haapi-stepper-form-field-select-label` | `HaapiStepperSelectFormFieldUI` | Select-specific labels | +| `.haapi-stepper-form-field-password-wrapper` | `HaapiStepperPasswordFormFieldUI` | Password input container | +| `.haapi-stepper-form-field-password-label` | `HaapiStepperPasswordFormFieldUI` | Password label | +| `.haapi-stepper-form-field-password-input` | `HaapiStepperPasswordFormFieldUI` | Password input | +| `.haapi-stepper-form-field-password-visibility-toggle` | `HaapiStepperPasswordFormFieldUI` | Password visibility toggle button | +| `.haapi-stepper-button` | `HaapiStepperFormUI` | Primary submit buttons | +| `.haapi-stepper-button-outline` | `HaapiStepperFormUI` | Outline/cancel buttons | +| `.haapi-stepper-well` | `Well` | Styled content container | +| `.haapi-stepper-links` | `HaapiStepperLinksUI` | Links container | +| `.haapi-stepper-link` | `HaapiStepperLinkUI` | Link element | +| `.haapi-stepper-link-qr-code` | `HaapiStepperLinkUI` | QR code link figure wrapper | +| `.haapi-stepper-link-qr-code-title` | `HaapiStepperLinkUI` | QR code link figcaption | +| `.haapi-stepper-link-qr-code-button` | `HaapiStepperLinkUI` | QR code link expand button | +| `.haapi-stepper-link-qr-code-dialog` | `HaapiStepperQrCodeLinkDialog` | Fullscreen QR code dialog | +| `.haapi-stepper-link-qr-code-dialog-close-button` | `HaapiStepperQrCodeLinkDialog` | Button wrapping the expanded QR code image; closes the dialog when clicked | +| `.haapi-stepper-link-qr-code-dialog-image` | `HaapiStepperQrCodeLinkDialog` | Fullscreen QR code dialog image | +| `.haapi-stepper-actions` | `HaapiStepperActionsUI` | Actions container | +| `.haapi-stepper-heading` | `HaapiStepperMessagesUI` | Heading messages | +| `.haapi-stepper-userName` | `HaapiStepperMessagesUI` | User name display | +| `.haapi-stepper-userCode` | `HaapiStepperMessagesUI` | User code display (e.g. recovery codes) | +| `.haapi-stepper-polling-progress` | `HaapiStepperClientOperationUI` | Remaining polling time indicator (e.g. recovery codes) | +| `.haapi-stepper-webauthn-registration-attachment-icon` | `HaapiStepperWebAuthnRegistrationAttachmentCard` | Attachment card icon | +| `.haapi-stepper-webauthn-registration-attachment-title` | `HaapiStepperWebAuthnRegistrationAttachmentCard` | Attachment card option label | +| `.haapi-stepper-webauthn-registration-attachment-description` | `HaapiStepperWebAuthnRegistrationAttachmentCard` | Attachment card option description | +| `.haapi-stepper-consent-logos` | `UserConsentViewNameBuiltInUI` | Container for user consent logos | +| `.haapi-stepper-error-boundary-fallback` | `DefaultErrorFallback` | Error boundary fallback container | +| `.haapi-validation-errors-container` | `HaapiStepperFormValidationErrorInputWrapper` | Wrapper around a form field that has validation errors. Receives the `.has-errors` modifier class while errors are visible | +| `.haapi-validation-errors` | `HaapiStepperFormValidationErrorInputWrapper` | Inner container that holds the list of validation error messages | +| `.haapi-validation-error` | `HaapiStepperFormValidationErrorInputWrapper` | A single validation error entry (also gets the utility classes `.red .py1`) | +| `.haapi-validation-error-description` | `HaapiStepperFormValidationErrorInputWrapper` | Validation error message text | + +
+ +### Customize with render interceptors + +Render interceptors are the programmatic way to customize the default step UI elements — loader, error, step, actions (form, client operation, selector), links, messages, and form fields. + +Each is a function that receives the `HaapiStepper` API data for the target UI element (`currentStep`, `loading`, `error`, `nextStep`…) and returns either a React element to replace the default UI element, the API data to render the default UI element, or `null` to skip the element from being rendered: + +```tsx + +
📨 {message.text}
} + /> +
+``` + +[See example](./docs/sections/02-examples/01-render-interceptors/MessageRenderInterceptorHaapiReactSDKPlaygroundExample.tsx) + +> 💡 **Design pattern note**: always return or pass through the API data. +> +> - **To override**: return your custom element. +> - **To delegate to the default renderer**: return the API data (`{ currentStep, history, loading, error, nextStep }`), optionally modified. +> - **To remove an element**: return `null`. + +### Customize with UI composition + +The declarative path to build UIs from scratch. Use it for what the API doesn't expose as elements — grouping (fieldsets/tabs), cross-element layouts, inserting your own elements, and behaviour customizations (tabs, multi-step wizards). Best for layout and complex customizations. + +Each HAAPI entity has a corresponding UI component (`HaapiStepperActionsUI`, `HaapiStepperMessagesUI`, `HaapiStepperLinksUI`…). `HaapiStepper` still runs the flow: + +```tsx +function LoginPage() { + const { currentStep, loading, nextStep } = useHaapiStepper(); + if (loading || !currentStep) return

Loading…

; + + const { actions, messages, links } = currentStep.dataHelpers; + + return ( + +

Sign in

+ + + +
+ ); +} + + + + +``` + +[See example](./docs/sections/01-api-reference/BuildingBlocksUICompositionHaapiReactSDKPlaygroundExample.tsx) + +### Mixed — combine the default with your own UI + +Render interceptors and UI composition aren't exclusive — mix them wherever it helps. For +example, return UI building blocks from a render interceptor to restructure *part* of the +default UI while keeping everything else: a form render interceptor that re-lays-out the same +fields, a step interceptor that swaps one step's UI, or the headless `HaapiStepper` driving a +mix of building blocks, plain HTML and third-party components. + +```tsx +// Keep the default everywhere, but give the form your own layout built from the building blocks. + { + const action = currentStep.dataHelpers.actions.form[0]; + return ( + + + + ); + }} +/> +``` + +[See example](./docs/sections/01-api-reference/01-ui-components/01-form-ui/FormUICompositionHaapiReactSDKPlaygroundExample.tsx) diff --git a/src/haapi-react-sdk/docs/DocsExamples.spec.tsx b/src/haapi-react-sdk/docs/DocsExamples.spec.tsx new file mode 100644 index 00000000..257d75cc --- /dev/null +++ b/src/haapi-react-sdk/docs/DocsExamples.spec.tsx @@ -0,0 +1,45 @@ +/* + * 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 { ComponentType } from 'react'; +import { render, cleanup } from '@testing-library/react'; +import { afterEach, describe, expect, test, vi } from 'vitest'; + +/* + * Smoke test for every runnable docs example: each `*HaapiReactSDKPlaygroundExample.tsx` must mount + * without crashing. The examples are typechecked by `tsc -b`, but only this spec executes them, so a + * runtime break (bad hook usage, a step the harness data doesn't cover) fails CI here instead of on the + * published docs page. Sandbox-only packages the examples import (antd, …) are aliased to + * `_harness/sandbox-package-stub.ts` in `vitest.config.ts`. + */ + +// The examples normally run against the playground's mocked web driver (docgen-side). Here the driver +// never resolves, so each example renders its initial UI — enough to catch mount-time crashes. +vi.mock('@curity/identityserver-haapi-web-driver', () => ({ + createHaapiFetch: () => () => new Promise(() => undefined), +})); + +const examples = import.meta.glob('./sections/**/*HaapiReactSDKPlaygroundExample.tsx'); + +describe('docs examples', () => { + afterEach(cleanup); + + test('the glob finds the examples', () => { + expect(Object.keys(examples).length).toBeGreaterThan(0); + }); + + for (const [file, load] of Object.entries(examples)) { + test(`${file} renders without crashing`, async () => { + const { default: App } = (await load()) as { default: ComponentType }; + expect(() => render()).not.toThrow(); + }); + } +}); diff --git a/src/haapi-react-sdk/docs/_harness/AutoSubmitForm.tsx b/src/haapi-react-sdk/docs/_harness/AutoSubmitForm.tsx new file mode 100644 index 00000000..c455e42d --- /dev/null +++ b/src/haapi-react-sdk/docs/_harness/AutoSubmitForm.tsx @@ -0,0 +1,80 @@ +/* + * 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 { ReactNode, useEffect, useRef } from 'react'; + +/** + * Previewer helper: clicks the rendered form's submit button once, as soon as it appears. + * + * Used so error examples surface their error without manual input — a HAAPI error only exists as the + * response to a submitted action. This stays *outside* the example so the example's own code remains + * a clean, documentation-grade ``; the "force" lives here, not in the example. + * + * The stepper boots asynchronously (it fetches the step before rendering the form), so a fixed delay is + * unreliable — a short one fires before the form exists, a long one adds visible lag. Instead we poll for + * the submit button and act the moment it renders, giving up after a few seconds. + */ +const POLL_INTERVAL_MS = 50; +const MAX_WAIT_MS = 5000; + +export function AutoSubmitForm({ children }: { children: ReactNode }) { + const containerRef = useRef(null); + + useEffect(() => { + let cancelled = false; + let waited = 0; + // One handle for the initial kick-off AND every retry, so cleanup always clears the pending timer. + let timeoutId: ReturnType; + + const trySubmit = () => { + if (cancelled) { + return; + } + const container = containerRef.current; + // Prefer the SDK's submit button; fall back to a plain submit button for examples with a custom form. + const submit = + container?.querySelector('[data-testid="form-submit-button"]') ?? + container?.querySelector('button[type="submit"]'); + + if (!submit) { + if ((waited += POLL_INTERVAL_MS) <= MAX_WAIT_MS) { + timeoutId = setTimeout(trySubmit, POLL_INTERVAL_MS); + } + return; + } + + // Fill text-like inputs first, otherwise the browser's required-field validation blocks the submit + // and no request is sent. Values are irrelevant — the mock returns its canned error regardless. Set + // via the native setter + an `input` event so React's controlled state updates too. + // eslint-disable-next-line @typescript-eslint/unbound-method -- always invoked via `.call(input, …)` below + const setNativeValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; + const fillableTypes = ['text', 'email', 'password', 'tel', 'url', 'number']; + container?.querySelectorAll('input').forEach(input => { + if (fillableTypes.includes(input.type) && !input.value) { + // Password fields get a unique filler: submitting a common word like "preview" as a password + // trips Chrome's data-breach warning dialog over the docs page. + setNativeValue?.call(input, input.type === 'password' ? 'preview-docs-playground-42!' : 'preview'); + input.dispatchEvent(new Event('input', { bubbles: true })); + } + }); + + submit.click(); + }; + + timeoutId = setTimeout(trySubmit, 0); + return () => { + cancelled = true; + clearTimeout(timeoutId); + }; + }, []); + + return
{children}
; +} diff --git a/src/haapi-react-sdk/docs/_harness/ExamplePreviewer.tsx b/src/haapi-react-sdk/docs/_harness/ExamplePreviewer.tsx new file mode 100644 index 00000000..f6602d9a --- /dev/null +++ b/src/haapi-react-sdk/docs/_harness/ExamplePreviewer.tsx @@ -0,0 +1,85 @@ +/* + * 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 { cloneElement, Fragment, ReactElement, useState } from 'react'; +import type { HaapiStepperConfig } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types'; +import { DEFAULT_EXAMPLE, EXAMPLES, HAAPI_EXAMPLE } from './catalog'; +import { StepSelect } from './StepSelect'; +import { StepDataDetails } from './StepDataDetails'; +import { bootstrapForStep } from './config'; +import { AutoSubmitForm } from './AutoSubmitForm'; + +/** + * Wraps a HAAPI example in the docs preview chrome — a step selector and a collapsed step-data view — and + * provides **served-mode** config: it points `window.__CONFIG__` at the selected step's bootstrap (the + * host app's job in production) before the wrapped `` mounts, so the example itself stays + * clean served code (``, no `config` prop). + * + * `defaultStep` sets which step the preview opens on; the reader can switch to any other from the + * selector. + */ +export function ExamplePreviewer({ + children, + defaultStep = DEFAULT_EXAMPLE, + autoSubmit = false, + showStepSelect = false, +}: { + children: ReactElement<{ config?: Partial }>; + /** + * Which example the preview opens on — a {@link HAAPI_EXAMPLE} key (a customization-pinned step for + * single-step examples, or a showcase step). Defaults to the first browsable showcase entry. + */ + defaultStep?: string; + /** Submit the step on mount so its post-submit error shows by default (e.g. error-component examples). */ + autoSubmit?: boolean; + /** + * Show the "Step to display" selector. Opt-in: only for examples meant to be browsed across steps + * (default UI, whole-step customizations). Examples pinned to one step omit it — switching would render + * nothing custom. + */ + showStepSelect?: boolean; +}) { + const [step, setStep] = useState(defaultStep); + + // Runs during render, before the child mounts, so it's set in time — an effect would be too late. + window.__CONFIG__ = bootstrapForStep(step); + + // Sandbox-safe config injected into every example at runtime (the visible snippet stays clean served + // code): autostart off (never fire a real WebAuthn/BankID ceremony on mount, which would render nothing + // while the browser prompts) and auto-redirect off (a completed flow shows the completed step instead of + // trying to follow an authorization-response link to a real redirect URI the mock doesn't have). + // The previewer wraps an arbitrary example element it doesn't render itself, so injecting the sandbox + // config via cloneElement is the point here. + // eslint-disable-next-line @eslint-react/no-clone-element + const example = cloneElement(children, { config: SANDBOX_CONFIG }); + + // Auto-submit when the prop opts in, or when the catalog entry marks the example as one whose whole + // point is the post-submit state (e.g. an authentication or validation error). + const shouldAutoSubmit = autoSubmit || EXAMPLES[step as HAAPI_EXAMPLE].autoSubmit === true; + + return ( + <> + {showStepSelect && } + + {/* key={step} remounts the example on change so the stepper re-reads window.__CONFIG__. */} + {shouldAutoSubmit ? {example} : example} + + {/* The selected step's HAAPI data, collapsed below the rendered UI. */} + + + ); +} + +const SANDBOX_CONFIG: Partial = { + webAuthnAutostart: false, + bankIdAutostart: false, + autoRedirectOnAuthenticationComplete: false, +}; diff --git a/src/haapi-react-sdk/docs/_harness/StepDataDetails.tsx b/src/haapi-react-sdk/docs/_harness/StepDataDetails.tsx new file mode 100644 index 00000000..f5459fa7 --- /dev/null +++ b/src/haapi-react-sdk/docs/_harness/StepDataDetails.tsx @@ -0,0 +1,73 @@ +/* + * 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 { HaapiStep } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-step.types'; + +// Minimal JSON token colours, readable on the translucent panel in both light and dark. +const JSON_COLORS = { key: '#9d174d', string: '#0b7285', number: '#b45309', keyword: '#6d28d9' }; + +/** Render a value as syntax-highlighted JSON HTML (keys, strings, numbers and keywords coloured). */ +function highlightJson(value: unknown): string { + const json = JSON.stringify(value, null, 2); + const escaped = json.replace(/&/g, '&').replace(//g, '>'); + return escaped.replace( + /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, + token => { + let color: string = JSON_COLORS.number; + if (token.startsWith('"')) { + color = token.endsWith(':') ? JSON_COLORS.key : JSON_COLORS.string; + } else if (token === 'true' || token === 'false' || token === 'null') { + color = JSON_COLORS.keyword; + } + return `${token}`; + } + ); +} + +/** + * A collapsed "See HAAPI step data" view of the raw HAAPI step the preview is rendering — a + * syntax-highlighted JSON panel shown below the form so readers can correlate the UI with its step data. + */ +export function StepDataDetails({ step }: { step: HaapiStep }) { + // Inline styles override the Curity stylesheet's global `details`/`summary` box so this docs panel + // reads as a deliberate, neutral card rather than a login-form field. + return ( +
+ + See HAAPI step data + +
s, so the markup
+        // below is entirely self-generated.
+        // eslint-disable-next-line @eslint-react/dom/no-dangerously-set-innerhtml
+        dangerouslySetInnerHTML={{ __html: highlightJson(step) }}
+      />
+    
+ ); +} diff --git a/src/haapi-react-sdk/docs/_harness/StepSelect.tsx b/src/haapi-react-sdk/docs/_harness/StepSelect.tsx new file mode 100644 index 00000000..8f9436a0 --- /dev/null +++ b/src/haapi-react-sdk/docs/_harness/StepSelect.tsx @@ -0,0 +1,74 @@ +/* + * 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 { EXAMPLES, HAAPI_EXAMPLE } from './catalog'; + +// The selector options, grouped by catalog section (preserving the catalog's declaration order). Only +// browsable `kind: 'step'` entries are listed. Each group becomes an ; each entry's key is the +// option value carried into the bootstrap URL. +const STEP_GROUPS: { section: string; options: { value: string; label: string }[] }[] = ( + Object.keys(EXAMPLES) as HAAPI_EXAMPLE[] +) + .filter(key => EXAMPLES[key].kind === 'step') + .reduce<{ section: string; options: { value: string; label: string }[] }[]>((groups, key) => { + const entry = EXAMPLES[key]; + const group = groups.find(candidate => candidate.section === entry.section); + const option = { value: key as string, label: entry.title }; + return group + ? groups.map(candidate => + candidate === group ? { ...candidate, options: [...candidate.options, option] } : candidate + ) + : [...groups, { section: entry.section, options: [option] }]; + }, []); + +interface StepSelectProps { + value: string; + onChange: (step: string) => void; +} + +/** A labelled dropdown for choosing which mocked HAAPI step the preview renders, grouped by section. */ +export function StepSelect({ value, onChange }: StepSelectProps) { + return ( +
+ + +
+ ); +} diff --git a/src/haapi-react-sdk/docs/_harness/catalog.ts b/src/haapi-react-sdk/docs/_harness/catalog.ts new file mode 100644 index 00000000..0a033760 --- /dev/null +++ b/src/haapi-react-sdk/docs/_harness/catalog.ts @@ -0,0 +1,2047 @@ +/* + * 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 { + HAAPI_POLLING_STATUS, + HAAPI_PROBLEM_STEPS, + HAAPI_STEPS, + HaapiActionStep, + HaapiAuthenticationFailedProblemStep, + HaapiErrorStep, + HaapiInputValidationProblemStep, + HaapiStep, +} from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-step.types'; +import { + HAAPI_ACTION_CLIENT_OPERATIONS, + HAAPI_ACTION_TYPES, + HAAPI_FORM_ACTION_KINDS, +} from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-action.types'; +import { + HAAPI_FORM_FIELDS, + HTTP_METHODS, +} from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types'; +import { MEDIA_TYPES } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/media.types'; + +/** + * The single source of truth for the docs' canned HAAPI example data. Every example the docs render — + * the browsable step showcase (the "Step to display" selector) and the steps pinned by the + * customization examples — is declared once here, keyed by {@link HAAPI_EXAMPLE}. + * + * This module owns the data outright: the step objects are copied in verbatim (it does not import them + * from the previewer), so the docs site is standalone. It only depends on `@curity/haapi-react-sdk` + * type/enum exports. Consumers: + * - the web-driver mock serves `EXAMPLES[key].step` for the booted example (and `errorOnSubmit` on a + * submit), + * - `StepSelect` lists the `kind: 'step'` entries, grouped by `section`, + * - `bootstrapForStep` / `ExamplePreviewer` carry the example key in the bootstrap URL, + * - `build-sandpack-sdk.mjs` emits `catalog.json` / `examples.json` from it. + */ + +// ── Step data: browsable showcase (Authenticators) ─────────────────────────── + +const authenticatorHtmlFormLogin: HaapiActionStep = { + links: [ + { + href: '/authentication/authenticate/user-pwd/forgot-password', + rel: 'forgot-password', + title: 'Forgot your password?', + }, + { + href: '/authentication/authenticate/user-pwd/forgot-account-id', + rel: 'forgot-account-id', + title: 'Forgot your username?', + }, + { + href: '/authentication/register/create/htmlSql', + rel: 'register-create', + title: 'Create account', + }, + ], + metadata: { + viewName: 'authenticator/html-form/authenticate/get', + }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Login', + model: { + href: '/authentication/authenticate/htmlSql', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Login', + fields: [ + { + name: 'userName', + type: HAAPI_FORM_FIELDS.USERNAME, + label: 'Username', + }, + { + name: 'password', + type: HAAPI_FORM_FIELDS.PASSWORD, + label: 'Password', + }, + ], + }, + }, + ], +}; + +const authenticatorHtmlFormAccountCreatedHyperlink: HaapiActionStep = { + messages: [ + { + text: 'Your account has been created!', + classList: ['heading'], + }, + { + text: 'An email with an activation link has been sent to you.', + classList: [], + }, + { + text: 'If you did not receive an email, check your spam folder.', + classList: [], + }, + ], + metadata: { + viewName: 'authenticator/html-form/create-account/post', + }, + type: HAAPI_STEPS.REGISTRATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CONTINUE, + model: { + href: '/authentication/authenticate/user-pwd', + method: HTTP_METHODS.GET, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Return to login', + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'user-register', + model: { + href: '/authentication/register/create/user-pwd/resend-activation-email', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Send a new activation email', + }, + }, + ], +}; + +const authenticatorHtmlFormOtpActivation: HaapiActionStep = { + messages: [ + { + text: 'Activate Account', + classList: ['heading'], + }, + { + text: 'An email with a one time password has been sent to you.', + classList: ['info'], + }, + { + text: 'If you did not receive an email, check your spam folder.', + classList: ['info'], + }, + ], + metadata: { + viewName: 'authenticator/html-form/account-activation/otp', + }, + type: HAAPI_STEPS.REGISTRATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'user-register', + model: { + actionTitle: 'Activate', + href: '/authentication/register/create/user-pwd/otp', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + fields: [ + { + name: 'otp', + type: HAAPI_FORM_FIELDS.TEXT, + label: 'Enter one time password', + kind: 'number', + minLength: 6, + maxLength: 6, + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'user-register', + model: { + actionTitle: 'Send a new activation email', + href: '/authentication/register/create/user-pwd/otp', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + fields: [ + { + name: 'resend', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'true', + }, + ], + }, + }, + ], +}; + +const authenticatorWebauthnRegistrationAnyDevice: HaapiActionStep = { + links: [ + { + href: '/authentication/authenticate/webauthn', + rel: 'restart', + title: 'Return to login', + }, + ], + metadata: { + viewName: 'authenticator/webauthn/register/get', + }, + type: HAAPI_STEPS.REGISTRATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.CLIENT_OPERATION, + kind: 'device-register', + title: 'Register new device', + model: { + name: HAAPI_ACTION_CLIENT_OPERATIONS.WEBAUTHN_REGISTRATION, + arguments: { + crossPlatformCredentialCreationOptions: { + publicKey: { + rp: { name: 'se.curity', id: 'localhost' }, + user: { + name: 'john', + displayName: 'john', + id: 'rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB_SwdZoQ', + }, + challenge: 'Gwo3GzyJ25yPXd9bZc_h6HcjkamZ5Ye0eRyFjGrOOEg', + pubKeyCredParams: [ + { alg: -7, type: 'public-key' }, + { alg: -8, type: 'public-key' }, + { alg: -35, type: 'public-key' }, + { alg: -36, type: 'public-key' }, + { alg: -257, type: 'public-key' }, + { alg: -258, type: 'public-key' }, + { alg: -259, type: 'public-key' }, + ], + hints: [], + excludeCredentials: [], + authenticatorSelection: { + authenticatorAttachment: 'cross-platform', + requireResidentKey: false, + residentKey: 'preferred', + userVerification: 'preferred', + }, + attestation: 'none', + extensions: {}, + }, + }, + platformCredentialCreationOptions: { + publicKey: { + rp: { name: 'se.curity', id: 'localhost' }, + user: { + name: 'john', + displayName: 'john', + id: 'rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB_SwdZoQ', + }, + challenge: 'dKRGJIGkHwuSuPlpMe867RVLywin8-LtMjX7yqX2nTo', + pubKeyCredParams: [ + { alg: -7, type: 'public-key' }, + { alg: -8, type: 'public-key' }, + { alg: -35, type: 'public-key' }, + { alg: -36, type: 'public-key' }, + { alg: -257, type: 'public-key' }, + { alg: -258, type: 'public-key' }, + { alg: -259, type: 'public-key' }, + ], + hints: [], + excludeCredentials: [], + authenticatorSelection: { + authenticatorAttachment: 'platform', + requireResidentKey: false, + residentKey: 'preferred', + userVerification: 'preferred', + }, + attestation: 'none', + extensions: {}, + }, + }, + }, + continueActions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CONTINUE, + title: 'Register new device', + model: { + href: 'https://localhost:8443/authentication/register/create/webauthn', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.JSON, + fields: [ + { name: 'platformCredential', type: HAAPI_FORM_FIELDS.CONTEXT }, + { name: 'crossPlatformCredential', type: HAAPI_FORM_FIELDS.CONTEXT }, + ], + }, + }, + ], + errorActions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.REDIRECT, + model: { + href: '/authentication/authenticate/webauthn?_force_external_browser_flow=true', + method: HTTP_METHODS.GET, + }, + }, + ], + }, + }, + ], +}; + +const authenticatorWebauthnRegistrationPasskeys: HaapiActionStep = { + links: [ + { + href: '/authentication/authenticate/webauthn', + rel: 'restart', + title: 'Return to login', + }, + ], + metadata: { + viewName: 'authenticator/webauthn/register/get', + }, + type: HAAPI_STEPS.REGISTRATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.CLIENT_OPERATION, + kind: 'device-register', + title: 'Register new device', + model: { + name: HAAPI_ACTION_CLIENT_OPERATIONS.WEBAUTHN_REGISTRATION, + arguments: { + credentialCreationOptions: { + publicKey: { + rp: { name: 'se.curity', id: 'localhost' }, + user: { + name: 'john', + displayName: 'john', + id: 'rl6rgi9NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB_SwdZoQ', + }, + challenge: 'EvdQtJxjTOEHT3fhLNZDVbfA5p2aThQDeAIq6BY3aHw', + pubKeyCredParams: [ + { alg: -7, type: 'public-key' }, + { alg: -8, type: 'public-key' }, + { alg: -35, type: 'public-key' }, + { alg: -36, type: 'public-key' }, + { alg: -257, type: 'public-key' }, + { alg: -258, type: 'public-key' }, + { alg: -259, type: 'public-key' }, + ], + hints: [], + excludeCredentials: [], + authenticatorSelection: { + requireResidentKey: false, + residentKey: 'preferred', + userVerification: 'required', + }, + attestation: 'none', + extensions: {}, + }, + }, + }, + continueActions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CONTINUE, + title: 'Register new device', + model: { + href: 'https://localhost:8443/authentication/register/create/webauthn', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.JSON, + fields: [{ name: 'credential', type: HAAPI_FORM_FIELDS.CONTEXT }], + }, + }, + ], + errorActions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.REDIRECT, + model: { + href: '/authentication/authenticate/webauthn?_force_external_browser_flow=true', + method: HTTP_METHODS.GET, + }, + }, + ], + }, + }, + ], +}; + +const authenticatorWebauthnRegistrationCompleted: HaapiActionStep = { + messages: [ + { + text: 'Your device has been registered!', + classList: ['heading'], + }, + ], + metadata: { + viewName: 'authenticator/webauthn/register/post', + }, + type: HAAPI_STEPS.REGISTRATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CONTINUE, + model: { + href: '/authentication/authenticate/webauthn', + method: HTTP_METHODS.GET, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Return to login', + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'continue-auto-login', + title: 'Proceed to login as testuser', + model: { + href: '/authentication/anonymous/webauthn/auto-login', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Proceed to login as testuser', + fields: [ + { + name: 'alsk', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'xDyzBWCaRrTVntzfz5oBQf3txVM1IVfm', + }, + ], + }, + }, + ], +}; + +const authenticatorDuoSelectDevice: HaapiActionStep = { + links: [ + { + href: '/authentication/anonymous/duo/info', + rel: 'register-create', + title: 'Register new device', + }, + ], + metadata: { + viewName: 'authenticator/duo/authenticate/select-device', + }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.SELECTOR, + kind: 'device-selector', + title: 'Device selection', + model: { + options: ['Phone 1 (+XX XXX XXX 204)', 'Phone 2 (+XX XXX XXX 305)'].map(title => ({ + template: HAAPI_ACTION_TYPES.SELECTOR, + kind: 'device-option', + title, + model: { + options: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Let Duo choose (push or phone)', + model: { + href: '/authentication/authenticate/duo/select-device', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Auto', + fields: [ + { + name: 'device', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'DPL9ZB1RC5I7DI4ANGW4', + }, + { + name: 'factor', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'auto', + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Send push notification', + model: { + href: '/authentication/authenticate/duo/select-device', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Send notification to device', + fields: [ + { + name: 'device', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'DPL9ZB1RC5I7DI4ANGW4', + }, + { + name: 'factor', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'push', + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Receive passcode via SMS', + model: { + href: '/authentication/authenticate/duo/select-device', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Submit', + fields: [ + { + name: 'device', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'DPL9ZB1RC5I7DI4ANGW4', + }, + { + name: 'factor', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'sms', + }, + ], + continueActions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Authenticate with passcode', + model: { + href: '/authentication/authenticate/duo/select-device', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Submit', + fields: [ + { + name: 'device', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'DPL9ZB1RC5I7DI4ANGW4', + }, + { + name: 'factor', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'passcode', + }, + { + name: 'passcode', + type: HAAPI_FORM_FIELDS.TEXT, + label: 'Passcode', + }, + ], + }, + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Authenticate with passcode', + model: { + href: '/authentication/authenticate/duo/select-device', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Submit', + fields: [ + { + name: 'device', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'DPL9ZB1RC5I7DI4ANGW4', + }, + { + name: 'factor', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'passcode', + }, + { + name: 'passcode', + type: HAAPI_FORM_FIELDS.TEXT, + label: 'Passcode', + }, + ], + }, + }, + ], + }, + })), + }, + }, + ], +}; + +const authenticatorEmailLinkWait: HaapiActionStep = { + messages: [ + { + text: 'Please authenticate using the email sent to: xxxx@xxxxple.com', + classList: [], + }, + ], + metadata: { + viewName: 'authenticator/email/link-wait/index', + }, + type: HAAPI_STEPS.POLLING, + properties: { + recipientOfCommunication: 'xxxx@xxxxple.com', + status: HAAPI_POLLING_STATUS.PENDING, + }, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'poll', + model: { + href: '/authentication/authenticate/email/link-wait', + method: HTTP_METHODS.GET, + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CANCEL, + title: 'Restart the process', + model: { + href: '/authentication/authenticate/email', + method: HTTP_METHODS.GET, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Restart the process', + }, + }, + ], +}; + +const authenticatorEmailOtp: HaapiActionStep = { + messages: [ + { + text: 'The OTP code was sent to the following recipient: xxxx@xxxxple.com', + classList: [], + }, + ], + links: [ + { + href: '/authentication/authenticate/email', + rel: 'restart', + title: 'Restart', + }, + ], + metadata: { + viewName: 'authenticator/email/enter-otp/index', + }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Enter the OTP code', + model: { + href: 'https://localhost:8443/authentication/authenticate/email/enter-otp', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Verify', + fields: [ + { + name: 'otp', + type: HAAPI_FORM_FIELDS.TEXT, + label: 'OTP code', + kind: 'number', + minLength: 8, + maxLength: 8, + }, + ], + }, + }, + ], +}; + +// ── Step data: browsable showcase (Authentication Actions) ─────────────────── + +const authenticationActionRequestAck: HaapiActionStep = { + messages: [ + { + text: 'Proceed?', + classList: ['heading'], + }, + ], + metadata: { + viewName: 'authentication-action/request-acknowledgement/index', + }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CONTINUE, + model: { + href: '/authentication/_action/req-ack', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'OK', + fields: [ + { + name: 'response', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'true', + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CANCEL, + model: { + href: '/authentication/_action/req-ack', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Cancel', + fields: [ + { + name: 'response', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'false', + }, + ], + }, + }, + ], +}; + +const authenticationActionResetPassword: HaapiActionStep = { + type: HAAPI_STEPS.AUTHENTICATION, + metadata: { + viewName: 'authentication-action/reset-password/index', + }, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'password-reset', + title: 'A password reset is required.', + model: { + href: '/authentication/_action/reset-pwd', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Submit', + fields: [ + { + name: 'password', + type: HAAPI_FORM_FIELDS.PASSWORD, + label: 'Password', + }, + { + name: 'password2', + type: HAAPI_FORM_FIELDS.PASSWORD, + label: 'Verify Password', + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CONTINUE, + model: { + href: '/authentication/_action/reset-pwd', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Skip', + fields: [ + { + name: '_skip', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'true', + }, + ], + }, + }, + ], +}; + +const authenticationActionOptInMfaSetup: HaapiActionStep = { + messages: [ + { + text: 'Setup 2-step verification', + classList: ['heading'], + }, + { + text: 'Increase security by adding a second factor', + classList: ['info'], + }, + ], + metadata: { + viewName: 'authentication-action/opt-in-mfa/setup', + }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.SELECTOR, + kind: HAAPI_FORM_ACTION_KINDS.AUTHENTICATOR_SELECTOR, + title: 'Available methods', + model: { + options: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.AUTHENTICATOR_SELECTOR_OPTION, + title: 'Username and password', + properties: { + authenticatorType: 'html-form', + }, + model: { + href: '/authentication/_action/opt-in/setup', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + fields: [ + { + name: 'acr', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'urn:se:curity:authentication:html-form:htmlSql', + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.AUTHENTICATOR_SELECTOR_OPTION, + title: 'Email magic link', + properties: { + authenticatorType: 'email', + }, + model: { + href: '/authentication/_action/opt-in-1/setup', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + fields: [ + { + name: 'acr', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'urn:se:curity:authentication:email:email1', + }, + ], + }, + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Skip setting up 2-step verification. This will make your account less secure.', + model: { + href: '/dev/authn/authenticate/_action/opt-in-1/setup', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Skip', + fields: [ + { + name: 'optOut', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'true', + }, + ], + }, + }, + ], +}; + +const authenticationActionOptInMfaSetupConfirm: HaapiActionStep = { + messages: [ + { + text: 'Confirm setup of 2-step verification, using method: Username and password', + classList: ['heading'], + }, + { + text: 'These recovery codes can be used to manage the 2-step verification methods. Keep these codes somewhere safe but accessible. Each code can be used only once. You can generate new codes at any point.', + classList: ['info'], + }, + { text: '45971393', classList: ['info', 'userCode'] }, + { text: '72615246', classList: ['info', 'userCode'] }, + { text: '21410037', classList: ['info', 'userCode'] }, + { text: '30506519', classList: ['info', 'userCode'] }, + { text: '06308798', classList: ['info', 'userCode'] }, + { text: '97952895', classList: ['info', 'userCode'] }, + { text: '78603802', classList: ['info', 'userCode'] }, + { text: '98246710', classList: ['info', 'userCode'] }, + { text: '46775720', classList: ['info', 'userCode'] }, + { text: '39896367', classList: ['info', 'userCode'] }, + ], + metadata: { + viewName: 'authentication-action/opt-in-mfa/setup-confirm', + }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + model: { + href: '/authentication/_action/opt-in/setup-confirm', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Confirm', + fields: [ + { + name: 'recoveryCodesBatchId', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: '003e9cb1-b77e-413f-b4d4-ed64bb6ee975', + }, + { + name: 'confirm', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'on', + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CANCEL, + model: { + href: '/authentication/_action/opt-in/setup-confirm', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Cancel', + fields: [ + { + name: 'cancel', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'true', + }, + ], + }, + }, + ], +}; + +const authenticationActionOptInMfaSelect: HaapiActionStep = { + messages: [ + { + text: 'Login', + classList: ['heading'], + }, + { + text: 'Choose one of the methods below to continue login.', + classList: [], + }, + ], + links: [ + { + href: '/authentication/authenticate/_action/opt-in/select-to-manage', + rel: 'manage', + title: 'Manage', + }, + ], + metadata: { + viewName: 'authentication-action/opt-in-mfa/select', + }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.SELECTOR, + kind: HAAPI_FORM_ACTION_KINDS.AUTHENTICATOR_SELECTOR, + title: 'Available methods', + model: { + options: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'Username and password', + properties: { + authenticatorType: 'html-form', + }, + model: { + href: '/authentication/authenticate/_action/opt-in/select', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + fields: [ + { + name: 'acr', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'urn:se:curity:authentication:html-form:htmlSql', + }, + ], + }, + }, + ], + }, + }, + ], +}; + +const authenticationActionSelector: HaapiActionStep = { + metadata: { + viewName: 'authentication-action/selector/index', + }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.SELECTOR, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Select an option', + model: { + options: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CONTINUE, + title: 'One', + model: { + href: '/authentication/_action/selector', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + fields: [ + { + name: 'selectedId', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: '2573473', + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CONTINUE, + title: 'Two', + model: { + href: '/authentication/_action/selector', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + fields: [ + { + name: 'selectedId', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: '2736481', + }, + ], + }, + }, + ], + }, + }, + ], +}; + +const authenticationActionSignUp: HaapiActionStep = { + metadata: { + viewName: 'authentication-action/signup/get', + }, + type: HAAPI_STEPS.REGISTRATION, // adapted from "registration-step" + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'user-register', + title: 'Create Account', + model: { + href: '/authentication/_action/signup', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Create Account', + fields: [ + { + name: 'username', + type: HAAPI_FORM_FIELDS.USERNAME, + label: 'Username', + }, + { + name: 'primaryEmail', + type: HAAPI_FORM_FIELDS.TEXT, + label: 'Email', + kind: 'email', + }, + { + name: 'primaryPhoneNumber', + type: HAAPI_FORM_FIELDS.TEXT, + label: 'Phone Number', + required: false, + kind: 'tel', + }, + { + name: 'password', + type: HAAPI_FORM_FIELDS.PASSWORD, + label: 'Password', + }, + ], + }, + }, + ], +}; + +// ── Step data: browsable showcase (Authentication) ─────────────────────────── + +const authenticationSelectAuthenticator: HaapiActionStep = { + metadata: { + viewName: 'views/select-authenticator/index', + }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.SELECTOR, + kind: 'authenticator-selector', + title: 'Login', + model: { + options: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'Google', + properties: { + authenticatorType: 'google', + }, + model: { + href: '/authentication/authenticate/google', + method: HTTP_METHODS.GET, + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'TOTP', + properties: { + authenticatorType: 'totp', + }, + model: { + href: '/authentication/authenticate/totp', + method: HTTP_METHODS.GET, + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'Username/password', + properties: { + authenticatorType: 'html-form', + }, + model: { + href: '/authentication/authenticate/user-pwd', + method: HTTP_METHODS.GET, + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'Email', + properties: { + authenticatorType: 'email', + }, + model: { + href: '/authentication/authenticate/email', + method: HTTP_METHODS.GET, + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'SMS', + properties: { + authenticatorType: 'sms', + }, + model: { + href: '/authentication/authenticate/sms', + method: HTTP_METHODS.GET, + }, + }, + ], + }, + }, + ], +}; + +// ── Step data: browsable showcase (OAuth) ──────────────────────────────────── + +const oauthUserConsent: HaapiActionStep = { + messages: [ + { + text: 'Haapi Public Client', + classList: ['heading'], + }, + { + text: 'wants to access your account:', + classList: [], + }, + { + text: 'testuser', + classList: ['userName'], + }, + { + text: 'The following permissions are requested by the above app. Please review these and consent if it is OK.', + classList: [], + }, + ], + metadata: { + viewName: 'views/oauth/consent', + }, + type: HAAPI_STEPS.USER_CONSENT, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'user-consent', + model: { + href: '/oauth/authorize', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Submit Consent', + fields: [ + { + name: 'state', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'R_1qKGdC0yOlxxpq1JTpcdDeqpReGOAZz4', + }, + { + name: 'submit_consent', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'submit_consent', + }, + { + name: 'consent.claim.sub', + type: HAAPI_FORM_FIELDS.CHECKBOX, + label: 'User ID', + checked: true, + readonly: true, + }, + { + name: 'consent.scope.read', + type: HAAPI_FORM_FIELDS.CHECKBOX, + label: 'read', + checked: true, + readonly: true, + }, + ], + }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CANCEL, + model: { + href: '/oauth/authorize', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Cancel', + fields: [ + { + name: 'state', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'R_1qKGdC0yOlxxpq1JTpcdDeqpReGOAZz4', + }, + { + name: 'cancel_consent', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'cancel_consent', + }, + ], + }, + }, + ], +}; + +// ── Step data: browsable showcase (Forms) ──────────────────────────────────── + +const formStepWithDifferentInputs: HaapiActionStep = { + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.REDIRECT, + title: 'Login', + model: { + href: '/postback', + method: HTTP_METHODS.POST, + fields: [ + { + type: HAAPI_FORM_FIELDS.SELECT, + name: 'character', + label: 'Character', + options: [ + { + label: 'Bart', + value: 'bart', + }, + { + label: 'Lisa', + value: 'lisa', + selected: true, + }, + ], + }, + { + type: HAAPI_FORM_FIELDS.TEXT, + name: 'something', + label: 'Something', + value: 'foo', + }, + { + type: HAAPI_FORM_FIELDS.CHECKBOX, + name: 'accept', + label: 'Accept', + value: 'yes', + }, + { + type: HAAPI_FORM_FIELDS.CHECKBOX, + name: 'always-checked', + label: 'No option', + value: 'yes', + checked: true, + readonly: true, + }, + ], + actionTitle: 'Submit', + }, + }, + ], +}; + +const formStepWithUsernamePassword: HaapiActionStep = { + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.REDIRECT, + title: 'Login', + model: { + href: '/postback', + method: HTTP_METHODS.POST, + fields: [ + { + type: HAAPI_FORM_FIELDS.USERNAME, + name: 'user', + label: 'Username', + placeholder: 'Enter your username', + }, + { + type: HAAPI_FORM_FIELDS.PASSWORD, + name: 'password', + }, + ], + actionTitle: 'Submit', + }, + }, + ], +}; + +const formStepWithHiddenField: HaapiActionStep = { + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.REDIRECT, + title: 'Form with hidden field', + model: { + href: '/postback', + method: HTTP_METHODS.POST, + fields: [ + { + name: 'secret', + type: HAAPI_FORM_FIELDS.HIDDEN, + value: 'value', + }, + ], + actionTitle: 'Submit', + }, + }, + ], +}; + +// ── Step data: customization-pinned steps ──────────────────────────────────── + +// A compact HTML-form login used by customization examples (render interceptors / UI composition). +const htmlFormLogin: HaapiActionStep = { + links: [ + { + href: '/authentication/authenticate/user-pwd/forgot-password', + rel: 'forgot-password', + title: 'Forgot your password?', + }, + { + href: '/authentication/authenticate/user-pwd/forgot-account-id', + rel: 'forgot-account-id', + title: 'Forgot your username?', + }, + { href: '/authentication/register/create/htmlSql', rel: 'register-create', title: 'Create account' }, + ], + metadata: { viewName: 'authenticator/html-form/authenticate/get' }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Login', + model: { + href: '/authentication/authenticate/htmlSql', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Login', + fields: [ + { name: 'userName', type: HAAPI_FORM_FIELDS.USERNAME, label: 'Username' }, + { name: 'password', type: HAAPI_FORM_FIELDS.PASSWORD, label: 'Password' }, + ], + }, + }, + ], +}; + +const selectAuthenticator: HaapiActionStep = { + metadata: { viewName: 'views/select-authenticator/index' }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.SELECTOR, + kind: 'authenticator-selector', + title: 'Login', + model: { + options: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'Google', + properties: { authenticatorType: 'google' }, + model: { href: '/authentication/authenticate/google', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'TOTP', + properties: { authenticatorType: 'totp' }, + model: { href: '/authentication/authenticate/totp', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'Username/password', + properties: { authenticatorType: 'html-form' }, + model: { href: '/authentication/authenticate/user-pwd', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'Email', + properties: { authenticatorType: 'email' }, + model: { href: '/authentication/authenticate/email', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'SMS', + properties: { authenticatorType: 'sms' }, + model: { href: '/authentication/authenticate/sms', method: HTTP_METHODS.GET }, + }, + ], + }, + }, + ], +}; + +const emailOtp: HaapiActionStep = { + messages: [{ text: 'The OTP code was sent to the following recipient: xxxx@xxxxple.com', classList: [] }], + links: [{ href: '/authentication/authenticate/email', rel: 'restart', title: 'Restart' }], + metadata: { viewName: 'authenticator/email/enter-otp/index' }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.LOGIN, + title: 'Enter the OTP code', + model: { + href: 'https://localhost:8443/authentication/authenticate/email/enter-otp', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Verify', + fields: [ + { name: 'otp', type: HAAPI_FORM_FIELDS.TEXT, label: 'OTP code', kind: 'number', minLength: 8, maxLength: 8 }, + ], + }, + }, + ], +}; + +// A login form whose field name (`user`) matches the validation error below, so the inline field +// error renders when the form is submitted. +const usernamePasswordForm: HaapiActionStep = { + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.REDIRECT, + title: 'Login', + model: { + href: '/postback', + method: HTTP_METHODS.POST, + fields: [ + // Not required, so an empty submit reaches the mock and triggers the validation error. + { + type: HAAPI_FORM_FIELDS.USERNAME, + name: 'user', + label: 'Username', + placeholder: 'Enter your username', + required: false, + }, + { type: HAAPI_FORM_FIELDS.PASSWORD, name: 'password', required: false }, + ], + actionTitle: 'Submit', + }, + }, + ], +}; + +const formWithDifferentInputs: HaapiActionStep = { + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.REDIRECT, + title: 'Login', + model: { + href: '/postback', + method: HTTP_METHODS.POST, + fields: [ + { + type: HAAPI_FORM_FIELDS.SELECT, + name: 'character', + label: 'Character', + options: [ + { label: 'Bart', value: 'bart' }, + { label: 'Lisa', value: 'lisa', selected: true }, + ], + }, + { type: HAAPI_FORM_FIELDS.TEXT, name: 'something', label: 'Something', value: 'foo' }, + { type: HAAPI_FORM_FIELDS.CHECKBOX, name: 'accept', label: 'Accept', value: 'yes' }, + { + type: HAAPI_FORM_FIELDS.CHECKBOX, + name: 'always-checked', + label: 'No option', + value: 'yes', + checked: true, + readonly: true, + }, + ], + actionTitle: 'Submit', + }, + }, + ], +}; + +// A polling step (e.g. email-link wait) — stays PENDING so the loading interceptor is visible. +const emailLinkWaitPolling: HaapiActionStep = { + messages: [{ text: 'Please authenticate using the email sent to: john@example.com', classList: [] }], + metadata: { viewName: 'authenticator/email/link-wait/index' }, + type: HAAPI_STEPS.POLLING, + properties: { recipientOfCommunication: 'john@example.com', status: HAAPI_POLLING_STATUS.PENDING }, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'poll', + model: { href: '/authentication/authenticate/email/link-wait', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CANCEL, + title: 'Restart the process', + model: { + href: '/authentication/authenticate/email', + method: HTTP_METHODS.GET, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Restart the process', + }, + }, + ], +}; + +// A WebAuthn step carrying a client-operation action (rendered as a button; autostart is disabled in +// the previewer's stepper config so it doesn't try to invoke WebAuthn). +const webauthnClientOperation: HaapiActionStep = { + links: [{ href: '/authentication/authenticate/webauthn', rel: 'restart', title: 'Return to login' }], + metadata: { viewName: 'authenticator/webauthn/register/get' }, + type: HAAPI_STEPS.REGISTRATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.CLIENT_OPERATION, + kind: 'device-register', + title: 'Register new device', + model: { + name: HAAPI_ACTION_CLIENT_OPERATIONS.WEBAUTHN_REGISTRATION, + arguments: { + credentialCreationOptions: { + publicKey: { + rp: { name: 'se.curity', id: 'localhost' }, + user: { name: 'john', displayName: 'john', id: 'rl6rgi9NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB_SwdZoQ' }, + challenge: 'EvdQtJxjTOEHT3fhLNZDVbfA5p2aThQDeAIq6BY3aHw', + pubKeyCredParams: [ + { alg: -7, type: 'public-key' }, + { alg: -257, type: 'public-key' }, + ], + hints: [], + excludeCredentials: [], + authenticatorSelection: { + requireResidentKey: false, + residentKey: 'preferred', + userVerification: 'required', + }, + attestation: 'none', + extensions: {}, + }, + }, + }, + continueActions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CONTINUE, + title: 'Register new device', + model: { + href: 'https://localhost:8443/authentication/register/create/webauthn', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.JSON, + fields: [{ name: 'credential', type: HAAPI_FORM_FIELDS.CONTEXT }], + }, + }, + ], + errorActions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.REDIRECT, + model: { + href: '/authentication/authenticate/webauthn?_force_external_browser_flow=true', + method: HTTP_METHODS.GET, + }, + }, + ], + }, + }, + ], +}; + +// A "group" authenticator step (templateArea `html1`) whose selector contains a PingFederate option +// alongside the rest — used by the group-authenticator custom-UI example. +const groupAuthenticator: HaapiActionStep = { + metadata: { templateArea: 'html1', viewName: 'authenticator/group/authenticate/get' }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.SELECTOR, + kind: 'authenticator-selector', + title: 'Choose how to authenticate', + model: { + options: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'PingFederate', + properties: { authenticatorType: 'pingfederate' }, + model: { href: '/authentication/authenticate/pingfederate', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'Username/password', + properties: { authenticatorType: 'html-form' }, + model: { href: '/authentication/authenticate/user-pwd', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'Email', + properties: { authenticatorType: 'email' }, + model: { href: '/authentication/authenticate/email', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'select-authenticator', + title: 'SMS', + properties: { authenticatorType: 'sms' }, + model: { href: '/authentication/authenticate/sms', method: HTTP_METHODS.GET }, + }, + ], + }, + }, + ], +}; + +// An authenticator-selector step (templateArea `lwa-dev`) whose options include BankID — used by the +// tabbed authenticator-selector custom-UI example. +const customAuthenticatorSelect: HaapiActionStep = { + metadata: { templateArea: 'lwa-dev', viewName: 'views/select-authenticator/index' }, + type: HAAPI_STEPS.AUTHENTICATION, + actions: [ + { + template: HAAPI_ACTION_TYPES.SELECTOR, + kind: HAAPI_FORM_ACTION_KINDS.AUTHENTICATOR_SELECTOR, + title: 'Login', + model: { + options: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.AUTHENTICATOR_SELECTOR_OPTION, + title: 'BankID', + properties: { authenticatorType: HAAPI_ACTION_CLIENT_OPERATIONS.BANKID }, + model: { href: '/authentication/authenticate/bankid', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.AUTHENTICATOR_SELECTOR_OPTION, + title: 'Username/password', + properties: { authenticatorType: 'html-form' }, + model: { href: '/authentication/authenticate/user-pwd', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.AUTHENTICATOR_SELECTOR_OPTION, + title: 'Email', + properties: { authenticatorType: 'email' }, + model: { href: '/authentication/authenticate/email', method: HTTP_METHODS.GET }, + }, + ], + }, + }, + ], +}; + +// QR code the BankID wait step exposes as an image-typed link. HAAPI delivers QR codes this way (a link +// whose media `type` starts with `image/`), so the SDK renders it as a scannable figure rather than a +// text link. This is a representative placeholder pattern for the docs preview, not a scannable code. +const BANKID_QR_CODE = + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOTIiIGhlaWdodD0iMTkyIiB2aWV3Qm94PSIwIDAgMTkyIDE5MiI+PHJlY3Qgd2lkdGg9IjE5MiIgaGVpZ2h0PSIxOTIiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNMTIgMTJoOHY4aC04ek0yMCAxMmg4djhoLTh6TTI4IDEyaDh2OGgtOHpNMzYgMTJoOHY4aC04ek00NCAxMmg4djhoLTh6TTUyIDEyaDh2OGgtOHpNNjAgMTJoOHY4aC04ek04NCAxMmg4djhoLTh6TTEwMCAxMmg4djhoLTh6TTEyNCAxMmg4djhoLTh6TTEzMiAxMmg4djhoLTh6TTE0MCAxMmg4djhoLTh6TTE0OCAxMmg4djhoLTh6TTE1NiAxMmg4djhoLTh6TTE2NCAxMmg4djhoLTh6TTE3MiAxMmg4djhoLTh6TTEyIDIwaDh2OGgtOHpNNjAgMjBoOHY4aC04ek03NiAyMGg4djhoLTh6TTg0IDIwaDh2OGgtOHpNMTI0IDIwaDh2OGgtOHpNMTcyIDIwaDh2OGgtOHpNMTIgMjhoOHY4aC04ek0yOCAyOGg4djhoLTh6TTM2IDI4aDh2OGgtOHpNNDQgMjhoOHY4aC04ek02MCAyOGg4djhoLTh6TTg0IDI4aDh2OGgtOHpNMTAwIDI4aDh2OGgtOHpNMTI0IDI4aDh2OGgtOHpNMTQwIDI4aDh2OGgtOHpNMTQ4IDI4aDh2OGgtOHpNMTU2IDI4aDh2OGgtOHpNMTcyIDI4aDh2OGgtOHpNMTIgMzZoOHY4aC04ek0yOCAzNmg4djhoLTh6TTM2IDM2aDh2OGgtOHpNNDQgMzZoOHY4aC04ek02MCAzNmg4djhoLTh6TTEwMCAzNmg4djhoLTh6TTEyNCAzNmg4djhoLTh6TTE0MCAzNmg4djhoLTh6TTE0OCAzNmg4djhoLTh6TTE1NiAzNmg4djhoLTh6TTE3MiAzNmg4djhoLTh6TTEyIDQ0aDh2OGgtOHpNMjggNDRoOHY4aC04ek0zNiA0NGg4djhoLTh6TTQ0IDQ0aDh2OGgtOHpNNjAgNDRoOHY4aC04ek05MiA0NGg4djhoLTh6TTEwMCA0NGg4djhoLTh6TTEyNCA0NGg4djhoLTh6TTE0MCA0NGg4djhoLTh6TTE0OCA0NGg4djhoLTh6TTE1NiA0NGg4djhoLTh6TTE3MiA0NGg4djhoLTh6TTEyIDUyaDh2OGgtOHpNNjAgNTJoOHY4aC04ek03NiA1Mmg4djhoLTh6TTEwMCA1Mmg4djhoLTh6TTEwOCA1Mmg4djhoLTh6TTEyNCA1Mmg4djhoLTh6TTE3MiA1Mmg4djhoLTh6TTEyIDYwaDh2OGgtOHpNMjAgNjBoOHY4aC04ek0yOCA2MGg4djhoLTh6TTM2IDYwaDh2OGgtOHpNNDQgNjBoOHY4aC04ek01MiA2MGg4djhoLTh6TTYwIDYwaDh2OGgtOHpNOTIgNjBoOHY4aC04ek0xMDggNjBoOHY4aC04ek0xMjQgNjBoOHY4aC04ek0xMzIgNjBoOHY4aC04ek0xNDAgNjBoOHY4aC04ek0xNDggNjBoOHY4aC04ek0xNTYgNjBoOHY4aC04ek0xNjQgNjBoOHY4aC04ek0xNzIgNjBoOHY4aC04ek03NiA2OGg4djhoLTh6TTEwMCA2OGg4djhoLTh6TTEwOCA2OGg4djhoLTh6TTEyIDc2aDh2OGgtOHpNMjAgNzZoOHY4aC04ek0zNiA3Nmg4djhoLTh6TTc2IDc2aDh2OGgtOHpNOTIgNzZoOHY4aC04ek0xMDAgNzZoOHY4aC04ek0xMjQgNzZoOHY4aC04ek0xMzIgNzZoOHY4aC04ek0xNTYgNzZoOHY4aC04ek0xNzIgNzZoOHY4aC04ek0yMCA4NGg4djhoLTh6TTM2IDg0aDh2OGgtOHpNNTIgODRoOHY4aC04ek02MCA4NGg4djhoLTh6TTc2IDg0aDh2OGgtOHpNODQgODRoOHY4aC04ek05MiA4NGg4djhoLTh6TTEwMCA4NGg4djhoLTh6TTE2NCA4NGg4djhoLTh6TTE3MiA4NGg4djhoLTh6TTEyIDkyaDh2OGgtOHpNMjAgOTJoOHY4aC04ek01MiA5Mmg4djhoLTh6TTYwIDkyaDh2OGgtOHpNNjggOTJoOHY4aC04ek03NiA5Mmg4djhoLTh6TTg0IDkyaDh2OGgtOHpNOTIgOTJoOHY4aC04ek0xMjQgOTJoOHY4aC04ek0xNDAgOTJoOHY4aC04ek0xNzIgOTJoOHY4aC04ek01MiAxMDBoOHY4aC04ek04NCAxMDBoOHY4aC04ek0xMDAgMTAwaDh2OGgtOHpNMTA4IDEwMGg4djhoLTh6TTE0MCAxMDBoOHY4aC04ek0xNDggMTAwaDh2OGgtOHpNMTY0IDEwMGg4djhoLTh6TTQ0IDEwOGg4djhoLTh6TTY4IDEwOGg4djhoLTh6TTc2IDEwOGg4djhoLTh6TTg0IDEwOGg4djhoLTh6TTkyIDEwOGg4djhoLTh6TTEwMCAxMDhoOHY4aC04ek0xMTYgMTA4aDh2OGgtOHpNMTY0IDEwOGg4djhoLTh6TTE3MiAxMDhoOHY4aC04ek03NiAxMTZoOHY4aC04ek05MiAxMTZoOHY4aC04ek0xMDAgMTE2aDh2OGgtOHpNMTA4IDExNmg4djhoLTh6TTExNiAxMTZoOHY4aC04ek0xNDggMTE2aDh2OGgtOHpNMTY0IDExNmg4djhoLTh6TTEyIDEyNGg4djhoLTh6TTIwIDEyNGg4djhoLTh6TTI4IDEyNGg4djhoLTh6TTM2IDEyNGg4djhoLTh6TTQ0IDEyNGg4djhoLTh6TTUyIDEyNGg4djhoLTh6TTYwIDEyNGg4djhoLTh6TTExNiAxMjRoOHY4aC04ek0xNTYgMTI0aDh2OGgtOHpNMTY0IDEyNGg4djhoLTh6TTE3MiAxMjRoOHY4aC04ek0xMiAxMzJoOHY4aC04ek02MCAxMzJoOHY4aC04ek03NiAxMzJoOHY4aC04ek04NCAxMzJoOHY4aC04ek0xMDggMTMyaDh2OGgtOHpNMTE2IDEzMmg4djhoLTh6TTEzMiAxMzJoOHY4aC04ek0xNDggMTMyaDh2OGgtOHpNMTU2IDEzMmg4djhoLTh6TTE3MiAxMzJoOHY4aC04ek0xMiAxNDBoOHY4aC04ek0yOCAxNDBoOHY4aC04ek0zNiAxNDBoOHY4aC04ek00NCAxNDBoOHY4aC04ek02MCAxNDBoOHY4aC04ek03NiAxNDBoOHY4aC04ek0xMDAgMTQwaDh2OGgtOHpNMTA4IDE0MGg4djhoLTh6TTExNiAxNDBoOHY4aC04ek0xMjQgMTQwaDh2OGgtOHpNMTMyIDE0MGg4djhoLTh6TTE1NiAxNDBoOHY4aC04ek0xNjQgMTQwaDh2OGgtOHpNMTIgMTQ4aDh2OGgtOHpNMjggMTQ4aDh2OGgtOHpNMzYgMTQ4aDh2OGgtOHpNNDQgMTQ4aDh2OGgtOHpNNjAgMTQ4aDh2OGgtOHpNNzYgMTQ4aDh2OGgtOHpNOTIgMTQ4aDh2OGgtOHpNMTQ4IDE0OGg4djhoLTh6TTEyIDE1Nmg4djhoLTh6TTI4IDE1Nmg4djhoLTh6TTM2IDE1Nmg4djhoLTh6TTQ0IDE1Nmg4djhoLTh6TTYwIDE1Nmg4djhoLTh6TTg0IDE1Nmg4djhoLTh6TTkyIDE1Nmg4djhoLTh6TTExNiAxNTZoOHY4aC04ek0xMjQgMTU2aDh2OGgtOHpNMTMyIDE1Nmg4djhoLTh6TTE1NiAxNTZoOHY4aC04ek0xNzIgMTU2aDh2OGgtOHpNMTIgMTY0aDh2OGgtOHpNNjAgMTY0aDh2OGgtOHpNNzYgMTY0aDh2OGgtOHpNMTA4IDE2NGg4djhoLTh6TTEzMiAxNjRoOHY4aC04ek0xNjQgMTY0aDh2OGgtOHpNMTcyIDE2NGg4djhoLTh6TTEyIDE3Mmg4djhoLTh6TTIwIDE3Mmg4djhoLTh6TTI4IDE3Mmg4djhoLTh6TTM2IDE3Mmg4djhoLTh6TTQ0IDE3Mmg4djhoLTh6TTUyIDE3Mmg4djhoLTh6TTYwIDE3Mmg4djhoLTh6TTkyIDE3Mmg4djhoLTh6TTEwMCAxNzJoOHY4aC04ek0xMjQgMTcyaDh2OGgtOHpNMTMyIDE3Mmg4djhoLTh6TTE0OCAxNzJoOHY4aC04ek0xNTYgMTcyaDh2OGgtOHoiIGZpbGw9IiMxMTE4MjciLz48L3N2Zz4='; + +// A BankID "waiting" polling step (the step fetched after selecting the BankID authenticator). Stays +// PENDING and carries a cancel action so the tabbed selector can cancel it when switching tabs. The QR +// code is exposed as an image-typed link, matching how HAAPI delivers BankID QR codes. +const bankidWaitStep: HaapiActionStep = { + messages: [{ text: 'Start the BankID app and scan the QR code to authenticate.', classList: [] }], + links: [{ href: BANKID_QR_CODE, rel: 'activate', type: 'image/svg+xml', title: 'Scan with the BankID app' }], + metadata: { viewName: 'authenticator/bankid/wait/index' }, + type: HAAPI_STEPS.POLLING, + properties: { status: HAAPI_POLLING_STATUS.PENDING }, + actions: [ + { + template: HAAPI_ACTION_TYPES.FORM, + kind: 'poll', + model: { href: '/authentication/authenticate/bankid/wait', method: HTTP_METHODS.GET }, + }, + { + template: HAAPI_ACTION_TYPES.FORM, + kind: HAAPI_FORM_ACTION_KINDS.CANCEL, + title: 'Cancel', + model: { + href: '/authentication/authenticate/bankid/cancel', + method: HTTP_METHODS.POST, + type: MEDIA_TYPES.FORM_URLENCODED, + actionTitle: 'Cancel', + }, + }, + ], +}; + +// ── Errors returned on form submit (see `errorOnSubmit`) ───────────────────── + +const authenticationFailedError: HaapiAuthenticationFailedProblemStep = { + type: HAAPI_PROBLEM_STEPS.INCORRECT_CREDENTIALS, + title: 'Authentication Failed', + messages: [{ text: 'The username or password you entered is incorrect', classList: ['is-error'] }], + links: [{ href: '/forgot-password', rel: 'forgot-password', title: 'Forgot Password?' }], +}; + +const inputValidationError: HaapiInputValidationProblemStep = { + type: HAAPI_PROBLEM_STEPS.INVALID_INPUT, + title: 'Invalid Input', + messages: [{ text: 'Please correct the errors below', classList: ['is-error'] }], + invalidFields: [{ name: 'user', reason: 'missing', detail: 'Username is required' }], + links: [{ href: '/forgot-password', rel: 'forgot-password', title: 'Forgot Password?' }], +}; + +/** + * Returned after a submitted action so a flow always lands on a real "completed" screen instead of + * erroring. Also the body of the COMPLETED example. + */ +export const completedSuccessStep: HaapiStep = { + type: HAAPI_STEPS.COMPLETED_WITH_SUCCESS, + properties: {}, + messages: [{ text: 'You are now signed in.', classList: [] }], +}; + +// ── Catalog ────────────────────────────────────────────────────────────────── + +/** A single example entry: the step the mock serves plus its display metadata and kind. */ +export interface ExampleEntry { + title: string; + /** The catalog section this example belongs to (used to group the "Step to display" selector). */ + section: string; + step: HaapiStep; + /** When set, the mock returns this problem step on a submitted action, so the form shows the error. */ + errorOnSubmit?: HaapiErrorStep; + /** + * Submit the step on mount so its post-submit state shows by default. Set for examples whose whole + * point is the result of submitting (e.g. an authentication or validation error) — not for plain forms + * that merely carry an `errorOnSubmit` (those should open showing the form). + */ + autoSubmit?: boolean; + /** + * `'step'` — a browsable step showcase, listed in the "Step to display" selector. `'customization'` — + * a step pinned by a customization example (not browsable; omitted from the selector). + */ + kind: 'step' | 'customization'; +} + +/** + * Every example the docs can render, keyed by a stable, url-safe slug. The slug travels in the + * bootstrap URL (`/${key}`) and the mock maps it back to the entry's step (longest key wins, so a + * nested slug beats a substring of a shorter one). + */ +export enum HAAPI_EXAMPLE { + // Browsable showcase — Authenticators + AUTHENTICATORS_HTML_FORM_LOGIN = 'authenticators/html-form/login', + AUTHENTICATORS_HTML_FORM_ACCOUNT_CREATED = 'authenticators/html-form/account-created-hyperlink', + AUTHENTICATORS_HTML_FORM_ACTIVATE_OTP = 'authenticators/html-form/activate-otp', + AUTHENTICATORS_WEBAUTHN_SELECT_DEVICE = 'authenticators/webauthn/registration-select-device', + AUTHENTICATORS_WEBAUTHN_PASSKEY = 'authenticators/webauthn/registration-passkey', + AUTHENTICATORS_WEBAUTHN_COMPLETED = 'authenticators/webauthn/registration-completed', + AUTHENTICATORS_DUO_SELECT_DEVICE = 'authenticators/duo-select-device', + AUTHENTICATORS_EMAIL_LINK_WAIT = 'authenticators/email/link-wait', + AUTHENTICATORS_EMAIL_OTP = 'authenticators/email/otp', + // Browsable showcase — Authentication Actions + AUTHENTICATION_ACTIONS_REQUEST_ACK = 'authentication-actions/request-acknowledgement', + AUTHENTICATION_ACTIONS_RESET_PASSWORD = 'authentication-actions/reset-password', + AUTHENTICATION_ACTIONS_OPT_IN_MFA_SETUP = 'authentication-actions/opt-in-mfa/setup', + AUTHENTICATION_ACTIONS_OPT_IN_MFA_SETUP_CONFIRM = 'authentication-actions/opt-in-mfa/setup-confirm', + AUTHENTICATION_ACTIONS_OPT_IN_MFA_SELECT = 'authentication-actions/opt-in-mfa/select', + AUTHENTICATION_ACTIONS_SELECTOR = 'authentication-actions/selector', + AUTHENTICATION_ACTIONS_SIGN_UP = 'authentication-actions/sign-up', + // Browsable showcase — Authentication + AUTHENTICATION_SELECT_AUTHENTICATOR = 'authentication/select-authenticator', + // Browsable showcase — OAuth + OAUTH_USER_CONSENT = 'oauth/user-consent', + // Browsable showcase — Forms + FORMS_DIFFERENT_INPUTS = 'forms/form-with-different-inputs', + FORMS_AUTHENTICATION_ERROR = 'forms/form-with-authentication-error', + FORMS_VALIDATION_ERROR = 'forms/form-with-validation-error', + FORMS_HIDDEN_FIELD = 'forms/form-with-hidden-field', + // Customization-pinned steps (not browsable) + HTML_FORM_LOGIN = 'html-form-login', + SELECT_AUTHENTICATOR = 'select-authenticator', + EMAIL_OTP = 'email-otp', + LOGIN_WITH_VALIDATION = 'login-with-validation', + DIFFERENT_INPUTS = 'different-inputs', + POLLING = 'polling', + CLIENT_OPERATION = 'client-operation', + GROUP_AUTHENTICATOR = 'group-authenticator', + CUSTOM_AUTHENTICATOR_SELECT = 'custom-authenticator-select', + BANKID_WAIT = 'bankid-wait', + COMPLETED = 'completed', +} + +/** The single source of truth: every example's step data, display metadata, and kind. */ +export const EXAMPLES: Record = { + // Browsable showcase — Authenticators + [HAAPI_EXAMPLE.AUTHENTICATORS_HTML_FORM_LOGIN]: { + title: 'Login', + section: 'Authenticators', + step: authenticatorHtmlFormLogin, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATORS_HTML_FORM_ACCOUNT_CREATED]: { + title: 'Account Created (hyperlink)', + section: 'Authenticators', + step: authenticatorHtmlFormAccountCreatedHyperlink, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATORS_HTML_FORM_ACTIVATE_OTP]: { + title: 'Activate (OTP)', + section: 'Authenticators', + step: authenticatorHtmlFormOtpActivation, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATORS_WEBAUTHN_SELECT_DEVICE]: { + title: 'Registration (select device)', + section: 'Authenticators', + step: authenticatorWebauthnRegistrationAnyDevice, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATORS_WEBAUTHN_PASSKEY]: { + title: 'Registration (Passkey)', + section: 'Authenticators', + step: authenticatorWebauthnRegistrationPasskeys, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATORS_WEBAUTHN_COMPLETED]: { + title: 'Registration Completed', + section: 'Authenticators', + step: authenticatorWebauthnRegistrationCompleted, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATORS_DUO_SELECT_DEVICE]: { + title: 'Duo - Select Device', + section: 'Authenticators', + step: authenticatorDuoSelectDevice, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATORS_EMAIL_LINK_WAIT]: { + title: 'Link Wait', + section: 'Authenticators', + step: authenticatorEmailLinkWait, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATORS_EMAIL_OTP]: { + title: 'OTP', + section: 'Authenticators', + step: authenticatorEmailOtp, + kind: 'step', + }, + // Browsable showcase — Authentication Actions + [HAAPI_EXAMPLE.AUTHENTICATION_ACTIONS_REQUEST_ACK]: { + title: 'Request Acknowledgement', + section: 'Authentication Actions', + step: authenticationActionRequestAck, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATION_ACTIONS_RESET_PASSWORD]: { + title: 'Reset Password', + section: 'Authentication Actions', + step: authenticationActionResetPassword, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATION_ACTIONS_OPT_IN_MFA_SETUP]: { + title: 'Setup', + section: 'Authentication Actions', + step: authenticationActionOptInMfaSetup, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATION_ACTIONS_OPT_IN_MFA_SETUP_CONFIRM]: { + title: 'Setup confirm', + section: 'Authentication Actions', + step: authenticationActionOptInMfaSetupConfirm, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATION_ACTIONS_OPT_IN_MFA_SELECT]: { + title: 'Select', + section: 'Authentication Actions', + step: authenticationActionOptInMfaSelect, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATION_ACTIONS_SELECTOR]: { + title: 'Selector', + section: 'Authentication Actions', + step: authenticationActionSelector, + kind: 'step', + }, + [HAAPI_EXAMPLE.AUTHENTICATION_ACTIONS_SIGN_UP]: { + title: 'Sign Up', + section: 'Authentication Actions', + step: authenticationActionSignUp, + kind: 'step', + }, + // Browsable showcase — Authentication + [HAAPI_EXAMPLE.AUTHENTICATION_SELECT_AUTHENTICATOR]: { + title: 'Select Authenticator', + section: 'Authentication', + step: authenticationSelectAuthenticator, + kind: 'step', + }, + // Browsable showcase — OAuth + [HAAPI_EXAMPLE.OAUTH_USER_CONSENT]: { title: 'User Consent', section: 'OAuth', step: oauthUserConsent, kind: 'step' }, + // Browsable showcase — Forms + [HAAPI_EXAMPLE.FORMS_DIFFERENT_INPUTS]: { + title: 'Form with Different Inputs', + section: 'Forms', + step: formStepWithDifferentInputs, + kind: 'step', + }, + [HAAPI_EXAMPLE.FORMS_AUTHENTICATION_ERROR]: { + title: 'Form with Authentication Error', + section: 'Forms', + step: formStepWithUsernamePassword, + errorOnSubmit: authenticationFailedError, + autoSubmit: true, + kind: 'step', + }, + [HAAPI_EXAMPLE.FORMS_VALIDATION_ERROR]: { + title: 'Form with Validation Error', + section: 'Forms', + step: formStepWithUsernamePassword, + errorOnSubmit: inputValidationError, + autoSubmit: true, + kind: 'step', + }, + [HAAPI_EXAMPLE.FORMS_HIDDEN_FIELD]: { + title: 'Form with Hidden Field', + section: 'Forms', + step: formStepWithHiddenField, + kind: 'step', + }, + // Customization-pinned steps + [HAAPI_EXAMPLE.HTML_FORM_LOGIN]: { + title: 'HTML Form Login', + section: 'Customizations', + step: htmlFormLogin, + errorOnSubmit: authenticationFailedError, + kind: 'customization', + }, + [HAAPI_EXAMPLE.SELECT_AUTHENTICATOR]: { + title: 'Select Authenticator', + section: 'Customizations', + step: selectAuthenticator, + kind: 'customization', + }, + [HAAPI_EXAMPLE.EMAIL_OTP]: { title: 'Email OTP', section: 'Customizations', step: emailOtp, kind: 'customization' }, + [HAAPI_EXAMPLE.LOGIN_WITH_VALIDATION]: { + title: 'Login with Validation', + section: 'Customizations', + step: usernamePasswordForm, + errorOnSubmit: inputValidationError, + autoSubmit: true, + kind: 'customization', + }, + [HAAPI_EXAMPLE.DIFFERENT_INPUTS]: { + title: 'Form with Different Inputs', + section: 'Customizations', + step: formWithDifferentInputs, + kind: 'customization', + }, + [HAAPI_EXAMPLE.POLLING]: { + title: 'Polling', + section: 'Customizations', + step: emailLinkWaitPolling, + kind: 'customization', + }, + [HAAPI_EXAMPLE.CLIENT_OPERATION]: { + title: 'Client Operation', + section: 'Customizations', + step: webauthnClientOperation, + kind: 'customization', + }, + [HAAPI_EXAMPLE.GROUP_AUTHENTICATOR]: { + title: 'Group Authenticator', + section: 'Customizations', + step: groupAuthenticator, + kind: 'customization', + }, + [HAAPI_EXAMPLE.CUSTOM_AUTHENTICATOR_SELECT]: { + title: 'Custom Authenticator Select', + section: 'Customizations', + step: customAuthenticatorSelect, + kind: 'customization', + }, + [HAAPI_EXAMPLE.BANKID_WAIT]: { + title: 'BankID Wait', + section: 'Customizations', + step: bankidWaitStep, + kind: 'customization', + }, + [HAAPI_EXAMPLE.COMPLETED]: { + title: 'Completed', + section: 'Customizations', + step: completedSuccessStep, + kind: 'customization', + }, +}; + +/** The example the browsable selector opens on (the first `kind: 'step'` entry). */ +export const DEFAULT_EXAMPLE: HAAPI_EXAMPLE = (Object.keys(EXAMPLES) as HAAPI_EXAMPLE[]).find( + key => EXAMPLES[key].kind === 'step' +)!; diff --git a/src/haapi-react-sdk/docs/_harness/config.ts b/src/haapi-react-sdk/docs/_harness/config.ts new file mode 100644 index 00000000..a229ccb1 --- /dev/null +++ b/src/haapi-react-sdk/docs/_harness/config.ts @@ -0,0 +1,30 @@ +/* + * 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 { HaapiStepperBootstrapConfig } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types'; + +/** + * The mock HAAPI client config every example runs against. Examples that also call `useHaapiFetch` + * directly (e.g. the tabbed authenticator selector, to cancel an in-progress authenticator) pass this so + * they reuse the stepper's single driver instance — the driver is a process-global singleton that only + * supports one configuration per page load, and it compares configs by value. + */ +export const MOCK_HAAPI = { clientId: 'docs', tokenEndpoint: 'https://mock.invalid/oauth/token' }; + +/** + * Served-mode bootstrap for a catalog example: the {@link HAAPI_EXAMPLE} key travels in `initialUrl`, + * which the docs mock maps back to the matching step (the single source of truth in `examples/catalog`). + * `ExamplePreviewer` assigns this to `window.__CONFIG__` so examples run in served mode with no `config` + * prop. + */ +export function bootstrapForStep(step: string): HaapiStepperBootstrapConfig { + return { initialUrl: `/${step}`, haapi: MOCK_HAAPI, theme: {} }; +} diff --git a/src/haapi-react-sdk/docs/_harness/sandbox-package-stub.ts b/src/haapi-react-sdk/docs/_harness/sandbox-package-stub.ts new file mode 100644 index 00000000..dc61125b --- /dev/null +++ b/src/haapi-react-sdk/docs/_harness/sandbox-package-stub.ts @@ -0,0 +1,46 @@ +/* + * 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. + */ + +/* + * Runtime stand-in for the sandbox-only packages the docs examples import (antd, @ant-design/icons, + * @google-recaptcha/react): the code playground installs them from the CDN, the workspace doesn't ship + * them. `vitest.config.ts` aliases those packages here so the docs-examples smoke test can execute the + * examples; every export just renders its children. Type-checking is unaffected — tsc types the imports + * from `third-party.d.ts`. + */ + +import type { ReactNode } from 'react'; + +interface StubProps { + children?: ReactNode; +} +const stub = ({ children }: StubProps): ReactNode => children ?? null; + +// antd +export const Button = stub; +export const Card = stub; +export const Select = stub; +export const Tabs = stub; +export const Typography = Object.assign(({ children }: StubProps): ReactNode => children ?? null, { + Title: stub, + Text: stub, + Paragraph: stub, +}); +export const List = Object.assign(({ children }: StubProps): ReactNode => children ?? null, { Item: stub }); + +// @ant-design/icons +export const PhoneOutlined = stub; + +// @google-recaptcha/react +export const GoogleReCaptchaProvider = stub; +export const GoogleReCaptchaCheckbox = stub; + +export default {}; diff --git a/src/haapi-react-sdk/docs/_harness/third-party.d.ts b/src/haapi-react-sdk/docs/_harness/third-party.d.ts new file mode 100644 index 00000000..277945e9 --- /dev/null +++ b/src/haapi-react-sdk/docs/_harness/third-party.d.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/* + * Shorthand ambient declarations for the third-party packages a few docs examples import. They keep the + * SDK's manifest free of doc-only devDependencies: the examples still compile (typed `any` for these + * packages), and the type-checking that matters — the examples' use of the SDK's own API — stays fully + * strict via the real `@curity/haapi-react-sdk/*` sources. The docs playground installs the real packages + * at runtime (see the docgen's KNOWN_DEPS), so misuse still surfaces there. + * + * If a real dependency on one of these is hoisted into the workspace by another package, TypeScript + * resolves its actual types instead — these shorthands are only the fallback. + */ + +declare module 'antd'; +declare module '@ant-design/icons'; +declare module '@google-recaptcha/react'; diff --git a/src/haapi-react-sdk/docs/sections/00-overview/DefaultRenderingHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/00-overview/DefaultRenderingHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..0b98b193 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/00-overview/DefaultRenderingHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,29 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { HaapiStepperStepUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI'; +import { ExamplePreviewer } from '../../_harness/ExamplePreviewer'; + +/** + * Default step rendering: `` renders the current HAAPI step out of the box. The + * stepper runs in served mode — `` supplies `window.__CONFIG__`, as the host app + * does in production, so no `config` prop is needed. + */ +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/00-overview/HaapiStepperPreviewHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/00-overview/HaapiStepperPreviewHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..d44e08b8 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/00-overview/HaapiStepperPreviewHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,28 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { HaapiStepperStepUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI'; +import { ExamplePreviewer } from '../../_harness/ExamplePreviewer'; + +/** + * A catalog of the HAAPI authentication steps' default UIs: pick a step to see how + * `` renders it out of the box. + */ +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/00-overview/README.md b/src/haapi-react-sdk/docs/sections/00-overview/README.md new file mode 100644 index 00000000..5926238e --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/00-overview/README.md @@ -0,0 +1,3 @@ +Overview of the HAAPI React SDK: what it is, its actors and customization model. + +Content comes from the SDK README; this folder holds the examples it embeds. diff --git a/src/haapi-react-sdk/docs/sections/00-overview/StepBuildingBlocksHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/00-overview/StepBuildingBlocksHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..2efd5579 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/00-overview/StepBuildingBlocksHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,49 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperMessagesUI } from '@curity/haapi-react-sdk/haapi-stepper/ui/messages/HaapiStepperMessagesUI'; +import { HaapiStepperActionsUI } from '@curity/haapi-react-sdk/haapi-stepper/ui/actions/HaapiStepperActionsUI'; +import { HaapiStepperLinksUI } from '@curity/haapi-react-sdk/haapi-stepper/ui/links/HaapiStepperLinksUI'; +import { ExamplePreviewer } from '../../_harness/ExamplePreviewer'; + +/** + * Building blocks: `HaapiStepper` runs the flow while you render the step from the SDK's collection UI + * components — `HaapiStepperMessagesUI`, `HaapiStepperActionsUI`, `HaapiStepperLinksUI` — driving each + * with the data from `currentStep.dataHelpers` and advancing with `nextStep`. + */ +function Step() { + const { currentStep, nextStep } = useHaapiStepper(); + if (!currentStep) { + return null; + } + + const { actions, messages, links } = currentStep.dataHelpers; + + return ( + <> + + + + + ); +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/00-overview/StylesButtonCustomizationHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/00-overview/StylesButtonCustomizationHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..fe9c4ba2 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/00-overview/StylesButtonCustomizationHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,45 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { HaapiStepperStepUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI'; +import { ExamplePreviewer } from '../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../_harness/catalog'; + +/** + * Styles customization: restyle the default UI with CSS only — no code changes. The SDK components emit + * plain `.haapi-stepper-*` class names, so overriding `.haapi-stepper-button` is enough to give the submit + * button a custom colour, shape and size. Edit the CSS to see the button update live. + */ +const customButtonStyles = ` + .haapi-stepper-button { + background: #6200ee; + border-radius: 999px; + padding-block: 0.85rem; + letter-spacing: 0.02em; + } + .haapi-stepper-button:hover { + background: #4b00b5; + } +`; + +export default function App() { + return ( + <> + + + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/CheckboxFieldRenderingHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/CheckboxFieldRenderingHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..0fdbff63 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/CheckboxFieldRenderingHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,64 @@ +/* + * 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 { Button } from 'antd'; +import { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperFormUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI'; +import { HaapiStepperFormFieldUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperFormFieldUI'; +import { HaapiStepperCheckboxFormFieldUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperCheckboxFormFieldUI'; +import { HAAPI_FORM_FIELDS } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types'; +import { ExamplePreviewer } from '../../../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../../../_harness/catalog'; + +/** Pair the checkbox with a terms description using `HaapiStepperCheckboxFormFieldUI`; keep the default for the rest. */ +function LoginForm() { + const { currentStep, nextStep } = useHaapiStepper(); + const formAction = currentStep?.dataHelpers.actions?.form?.[0]; + + if (!formAction) { + return null; + } + + return ( + + {({ fields }) => ( + <> + {fields.map(field => + field.type === HAAPI_FORM_FIELDS.CHECKBOX ? ( +
+ + + Read our terms and conditions. + +
+ ) : ( + + ) + )} + + + )} +
+ ); +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormFieldRenderInterceptorHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormFieldRenderInterceptorHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..a5f258b3 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormFieldRenderInterceptorHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,72 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperFormUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI'; +import type { HaapiStepperFormFieldRenderInterceptor } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types'; +import { HAAPI_FORM_FIELDS } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types'; +import { ExamplePreviewer } from '../../../../_harness/ExamplePreviewer'; + +/** + * UI Customization with render interceptor: per-field customization with `formFieldRenderInterceptor` — + * relabel the username field, replace the password field with a custom input wired to the built-in + * `formState`, and delegate every other field to the default rendering. + */ +function LoginForm() { + const { currentStep, nextStep } = useHaapiStepper(); + const formAction = currentStep?.dataHelpers.actions?.form?.[0]; + + if (!formAction) { + return null; + } + + const formFieldRenderInterceptor: HaapiStepperFormFieldRenderInterceptor = (field, formState) => { + if (field.type === HAAPI_FORM_FIELDS.USERNAME) { + return { ...field, label: 'Account', placeholder: 'user@example.com' }; + } + + if (field.type === HAAPI_FORM_FIELDS.PASSWORD) { + return ( + + ); + } + + // Delegate to default field rendering + return field; + }; + + return ( + + ); +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormStateHookUsageHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormStateHookUsageHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..6f8b44d0 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormStateHookUsageHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,91 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperStepUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI'; +import { useHaapiStepperFormState } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormHook'; +import { HAAPI_FORM_FIELDS } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types'; +import type { + HaapiStepperFormAction, + HaapiStepperNextStep, +} from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types'; +import { ExamplePreviewer } from '../../../../_harness/ExamplePreviewer'; + +/** + * A fully custom form built on `useHaapiStepperFormState`: the hook seeds the state from the action's + * fields and returns `get`/`set` helpers for the inputs plus the `values` map the action expects as its + * submission payload — so you own the markup while the SDK owns the form state. + */ +function CustomLoginForm({ + action, + onSubmit, +}: { + action: HaapiStepperFormAction; + onSubmit: HaapiStepperNextStep; +}) { + const fields = action.model.fields ?? []; + const formState = useHaapiStepperFormState(fields); + const username = fields.find(field => field.type === HAAPI_FORM_FIELDS.USERNAME)!; + const password = fields.find(field => field.type === HAAPI_FORM_FIELDS.PASSWORD)!; + + return ( +
{ + event.preventDefault(); + onSubmit(action, formState.values); + }} + > + + + +
+ ); +} + +function LoginForm() { + const { currentStep, nextStep } = useHaapiStepper(); + const formAction = currentStep?.dataHelpers.actions?.form?.[0]; + + if (!formAction) { + // Not a form step — delegate to the default step UI. + return ; + } + + return ; +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormSubmitBehaviorHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormSubmitBehaviorHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..ce82ca13 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormSubmitBehaviorHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,51 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperFormUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI'; +import type { + HaapiStepperFormAction, + HaapiStepperNextStep, +} from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/haapi-stepper.types'; +import { ExamplePreviewer } from '../../../../_harness/ExamplePreviewer'; + +/** + * Behaviour override around submission: wrap `onSubmit` to run your own logic (here a confirmation prompt; + * could be analytics or pre-submit validation) before delegating to `nextStep`. The default form UI and + * state management are untouched. + */ +function LoginForm() { + const { currentStep, nextStep } = useHaapiStepper(); + const formAction = currentStep?.dataHelpers.actions?.form?.[0]; + + if (!formAction) { + return null; + } + + const handleSubmit: HaapiStepperNextStep = (action, payload) => { + if (confirm('Submit the login form?')) { + nextStep(action, payload); + } + }; + + return ; +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormUICompositionHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormUICompositionHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..896654da --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/FormUICompositionHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,59 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperStepUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI'; +import { HaapiStepperFormUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI'; +import { HaapiStepperFormFieldUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperFormFieldUI'; +import { ExamplePreviewer } from '../../../../_harness/ExamplePreviewer'; + +/** + * UI Customization with UI composition: form customization with the `HaapiStepperFormUI` children render + * prop — lay the fields out in your own markup (here a `
`) while the built-in + * `HaapiStepperFormFieldUI` still renders each field and `HaapiStepperFormUI` manages the form state and + * submission. Steps without a form action fall back to the default `HaapiStepperStepUI`. + */ +function LoginForm() { + const { currentStep, nextStep } = useHaapiStepper(); + const formAction = currentStep?.dataHelpers.actions?.form?.[0]; + + if (!formAction) { + // Not a form step — delegate to the default step UI. + return ; + } + + return ( + + {({ fields }) => ( +
+ Sign in + {fields.map(field => ( + + ))} + +
+ )} +
+ ); +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/PasswordFieldRenderingHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/PasswordFieldRenderingHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..b70661c8 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/PasswordFieldRenderingHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,59 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperFormUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI'; +import { HaapiStepperFormFieldUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperFormFieldUI'; +import { HaapiStepperPasswordFormFieldUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperPasswordFormFieldUI'; +import { HaapiStepperFormSubmitButton } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton'; +import { HAAPI_FORM_FIELDS } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types'; +import { ExamplePreviewer } from '../../../../_harness/ExamplePreviewer'; + +/** Add a "forgot password?" link under the password field with `HaapiStepperPasswordFormFieldUI`; keep the default for the rest. */ +function LoginForm() { + const { currentStep, nextStep } = useHaapiStepper(); + const formAction = currentStep?.dataHelpers.actions?.form?.[0]; + + if (!formAction) { + return null; + } + + return ( + + {({ fields }) => ( + <> + {fields.map(field => + field.type === HAAPI_FORM_FIELDS.PASSWORD ? ( +
+ + Forgot your password? +
+ ) : ( + + ) + )} + + + )} +
+ ); +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/README.md b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/README.md new file mode 100644 index 00000000..b1a17b6f --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/README.md @@ -0,0 +1,7 @@ +1. HaapiStepperFormFieldUI +2. HaapiStepperTextFormFieldUI +3. HaapiStepperPasswordFormFieldUI +4. HaapiStepperSelectFormFieldUI +5. HaapiStepperCheckboxFormFieldUI +6. HaapiStepperFormSubmitButton +7. useHaapiStepperFormState diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/SelectFieldRenderingHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/SelectFieldRenderingHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..ed899f1e --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/SelectFieldRenderingHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,60 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperFormUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI'; +import { HaapiStepperFormFieldUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperFormFieldUI'; +import { HaapiStepperSelectFormFieldUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperSelectFormFieldUI'; +import { HaapiStepperFormSubmitButton } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton'; +import { HAAPI_FORM_FIELDS } from '@curity/haapi-react-sdk/haapi-stepper/data-access/types/haapi-form.types'; +import { ExamplePreviewer } from '../../../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../../../_harness/catalog'; + +/** Pair the select field with a hint using `HaapiStepperSelectFormFieldUI`; keep the default for the rest. */ +function LoginForm() { + const { currentStep, nextStep } = useHaapiStepper(); + const formAction = currentStep?.dataHelpers.actions?.form?.[0]; + + if (!formAction) { + return null; + } + + return ( + + {({ fields }) => ( + <> + {fields.map(field => + field.type === HAAPI_FORM_FIELDS.SELECT ? ( +
+ +

You can change this later in your settings.

+
+ ) : ( + + ) + )} + + + )} +
+ ); +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/SubmitButtonCustomizationHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/SubmitButtonCustomizationHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..af1783e3 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/01-form-ui/SubmitButtonCustomizationHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,56 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperStepUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI'; +import { HaapiStepperFormUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormUI'; +import { HaapiStepperFormSubmitButton } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormSubmitButton'; +import { HaapiStepperFormFieldUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/fields/HaapiStepperFormFieldUI'; +import { ExamplePreviewer } from '../../../../_harness/ExamplePreviewer'; + +/** + * Compose your own form layout with the `children` render interceptor while keeping the SDK's submit + * button: `HaapiStepperFormSubmitButton` reads the action from the form context, so it keeps the default + * label, icon and styling — customize them via `label`, `icon`, `children` or any native ` + + )} + + ); +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/ActionsUiUsageHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/ActionsUiUsageHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..5094db0a --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/ActionsUiUsageHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,38 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperActionsUI } from '@curity/haapi-react-sdk/haapi-stepper/ui/actions/HaapiStepperActionsUI'; +import { ExamplePreviewer } from '../../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../../_harness/catalog'; + +/** + * Render a step's actions (form, selector, and client-operation) with the `HaapiStepperActionsUI` + * building block: read `actions.all` from `dataHelpers` and pass it, plus `nextStep`, to the component — + * each action renders with the default UI for its subtype. + */ +function StepActions() { + const { currentStep, nextStep } = useHaapiStepper(); + const actions = currentStep?.dataHelpers.actions?.all; + + return ; +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/ClientOperationUiUsageHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/ClientOperationUiUsageHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..5b031cc7 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/ClientOperationUiUsageHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,41 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperClientOperationUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI'; +import { ExamplePreviewer } from '../../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../../_harness/catalog'; + +/** + * Render a step's client-operation action (e.g. WebAuthn, BankID) with the `HaapiStepperClientOperationUI` + * building block: read the action from `dataHelpers` and pass it, plus `nextStep`, to the component. + */ +function ClientOperationStep() { + const { currentStep, nextStep } = useHaapiStepper(); + const clientOperationAction = currentStep?.dataHelpers.actions?.clientOperation?.[0]; + + if (!clientOperationAction) { + return null; + } + + return ; +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/ErrorNotifierHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/ErrorNotifierHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..5cdedf07 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/ErrorNotifierHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,33 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { HaapiStepperStepUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/steps/HaapiStepperStepUI'; +import { HaapiStepperErrorNotifier } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperErrorNotifier'; +import { ExamplePreviewer } from '../../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../../_harness/catalog'; + +/** + * `HaapiStepperErrorNotifier` wraps your app and surfaces HAAPI errors as a dismissible toast. Submit + * the login form below — the mock rejects the credentials, and the resulting error pops up as a + * notification while the step UI stays in place. + */ +export default function App() { + return ( + + + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/FormValidationErrorWrapperHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/FormValidationErrorWrapperHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..d87548e7 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/FormValidationErrorWrapperHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,60 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperFormValidationErrorInputWrapper } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/form/HaapiStepperFormValidationErrorInputWrapper'; +import { ExamplePreviewer } from '../../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../../_harness/catalog'; + +/** + * `HaapiStepperFormValidationErrorInputWrapper` renders server-side field validation errors beneath the + * matching input. Submit the form — the mock returns a validation error for the `user` field, which the + * wrapper shows under the username input (other fields are unaffected). + */ +function LoginForm() { + const { currentStep, nextStep } = useHaapiStepper(); + const formAction = currentStep?.dataHelpers.actions?.form[0]; + + if (!formAction) { + return null; + } + + return ( +
{ + event.preventDefault(); + nextStep(formAction); + }} + > + + + + + +
+ ); +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/LinkUiUsageHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/LinkUiUsageHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..673f50f1 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/LinkUiUsageHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,41 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperLinksUI } from '@curity/haapi-react-sdk/haapi-stepper/ui/links/HaapiStepperLinksUI'; +import { ExamplePreviewer } from '../../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../../_harness/catalog'; + +/** + * Render a step's links (e.g. "Forgot password?", "Create account") with the `HaapiStepperLinksUI` + * building block: read `links` from `dataHelpers` and pass them, plus `nextStep`, to the component. + */ +function StepLinks() { + const { currentStep, nextStep } = useHaapiStepper(); + const links = currentStep?.dataHelpers.links; + + if (!links?.length) { + return null; + } + + return ; +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/MessagesRenderingHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/MessagesRenderingHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..72948597 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/MessagesRenderingHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,34 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperMessagesUI } from '@curity/haapi-react-sdk/haapi-stepper/ui/messages/HaapiStepperMessagesUI'; +import { ExamplePreviewer } from '../../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../../_harness/catalog'; + +/** Render the current step's messages with the `HaapiStepperMessagesUI` building block. */ +function Messages() { + const { currentStep } = useHaapiStepper(); + const messages = currentStep?.dataHelpers.messages; + + return ; +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/README.md b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/README.md new file mode 100644 index 00000000..c9eccbb3 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/README.md @@ -0,0 +1,67 @@ +# HAAPI stepper UI components + +The HAAPI React SDK provides a set of common HAAPI Stepper UI components that let you build highly +customized UIs while still relying on the SDK's defaults. They are the building blocks +`HaapiStepperStepUI` is made of, and what you compose your own UI from with `useHaapiStepper`. + +## Naming convention + +The HAAPI Stepper UI components are the UI representation of the main HAAPI entities, named with a +`UI` suffix: `HaapiStepperStepUI` displays/interacts with a `HaapiStep`, `HaapiStepperLinkUI` with a +`HaapiLink`, and so on. Collection components use the plural form (`HaapiStepperActionsUI`, +`HaapiStepperLinksUI`, `HaapiStepperMessagesUI`). + +## Usage + +Compose the building blocks to customize a step. For example, pass a `children` render function to +`HaapiStepperFormUI` to swap in your own username field and an Ant Design submit button, while the +built-in `HaapiStepperFormFieldUI` still manages the values and submission: + +```tsx +import { Button } from 'antd'; + +function LoginForm() { + const { currentStep, nextStep } = useHaapiStepper(); + const formAction = currentStep?.dataHelpers.actions.form[0]; + if (!formAction) return null; + + return ( + + {({ fields, formState }) => { + const username = fields.find(field => field.type === HAAPI_FORM_FIELDS.USERNAME); + const otherFields = fields.filter(field => field !== username); + + return ( + <> + {// Custom username field, wired to the built-in form state } + {username && } + + {// Remaining fields keep the default rendering } + {otherFields.map(field => ( + + ))} + + {// Ant Design submit button (submits the form via type="submit") } + + + ); + }} + + ); +} +``` +{@see_example ./docs/sections/01-api-reference/01-ui-components/UiComponentsCompositionUsageHaapiReactSDKPlaygroundExample.tsx} + +For styling, see the CSS Customization section in the Overview. + +1. HaapiStepperActionsUI +2. HaapiStepperSelectorUI +3. HaapiStepperClientOperationUI +4. HaapiStepperLinksUI +5. HaapiStepperLinkUI +6. HaapiStepperMessagesUI +7. HaapiStepperMessageUI +8. HaapiStepperErrorNotifier +9. HaapiStepperFormValidationErrorInputWrapper diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/SelectorUiUsageHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/SelectorUiUsageHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..f06fdc2a --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/SelectorUiUsageHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,42 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperSelectorUI } from '@curity/haapi-react-sdk/haapi-stepper/feature/actions/selector/HaapiStepperSelectorUI'; +import { ExamplePreviewer } from '../../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../../_harness/catalog'; + +/** + * Render a step's authenticator selector with the `HaapiStepperSelectorUI` building block: read the + * selector action from `dataHelpers` and pass it, plus `nextStep`, to the component. Picking an option + * advances the flow. + */ +function SelectorStep() { + const { currentStep, nextStep } = useHaapiStepper(); + const selectorAction = currentStep?.dataHelpers.actions?.selector?.[0]; + + if (!selectorAction) { + return null; + } + + return ; +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/SingleLinkUiUsageHaapiReactSDKPlaygroundExample.tsx b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/SingleLinkUiUsageHaapiReactSDKPlaygroundExample.tsx new file mode 100644 index 00000000..032993b8 --- /dev/null +++ b/src/haapi-react-sdk/docs/sections/01-api-reference/01-ui-components/SingleLinkUiUsageHaapiReactSDKPlaygroundExample.tsx @@ -0,0 +1,47 @@ +/* + * 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 { HaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepper'; +import { useHaapiStepper } from '@curity/haapi-react-sdk/haapi-stepper/feature/stepper/HaapiStepperHook'; +import { HaapiStepperLinkUI } from '@curity/haapi-react-sdk/haapi-stepper/ui/links/HaapiStepperLinkUI'; +import { ExamplePreviewer } from '../../../_harness/ExamplePreviewer'; +import { HAAPI_EXAMPLE } from '../../../_harness/catalog'; + +/** + * Render each of a step's links yourself with the `HaapiStepperLinkUI` building block — here inside a + * custom `