11/*
2- * Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
2+ * Copyright (c) 2025-2026 Ping Identity Corporation. All rights reserved.
33 *
44 * This software may be modified and distributed under the terms
55 * of the MIT license. See the LICENSE file for details.
66 */
77import './style.css' ;
88
99import { journey } from '@forgerock/journey-client' ;
10+ import { WebAuthn , WebAuthnStepType } from '@forgerock/journey-client/webauthn' ;
1011
1112import type { JourneyClient , RequestMiddleware } from '@forgerock/journey-client/types' ;
1213
1314import { renderCallbacks } from './callback-map.js' ;
15+ import { renderDeleteDevicesSection } from './components/delete-device.js' ;
1416import { renderQRCodeStep } from './components/qr-code.js' ;
1517import { renderRecoveryCodesStep } from './components/recovery-codes.js' ;
18+ import { deleteWebAuthnDevice } from './services/delete-webauthn-device.js' ;
19+ import { webauthnComponent } from './components/webauthn-step.js' ;
1620import { serverConfigs } from './server-configs.js' ;
1721
1822const qs = window . location . search ;
@@ -70,34 +74,6 @@ if (searchParams.get('middleware') === 'true') {
7074 }
7175 let step = await journeyClient . start ( { journey : journeyName } ) ;
7276
73- function renderComplete ( ) {
74- if ( step ?. type !== 'LoginSuccess' ) {
75- throw new Error ( 'Expected step to be defined and of type LoginSuccess' ) ;
76- }
77-
78- const session = step . getSessionToken ( ) ;
79-
80- console . log ( `Session Token: ${ session || 'none' } ` ) ;
81-
82- journeyEl . innerHTML = `
83- <h2 id="completeHeader">Complete</h2>
84- <span id="sessionLabel">Session:</span>
85- <pre id="sessionToken" id="sessionToken">${ session } </pre>
86- <button type="button" id="logoutButton">Logout</button>
87- ` ;
88-
89- const loginBtn = document . getElementById ( 'logoutButton' ) as HTMLButtonElement ;
90- loginBtn . addEventListener ( 'click' , async ( ) => {
91- await journeyClient . terminate ( ) ;
92-
93- console . log ( 'Logout successful' ) ;
94-
95- step = await journeyClient . start ( { journey : journeyName } ) ;
96-
97- renderForm ( ) ;
98- } ) ;
99- }
100-
10177 function renderError ( ) {
10278 if ( step ?. type !== 'LoginFailure' ) {
10379 throw new Error ( 'Expected step to be defined and of type LoginFailure' ) ;
@@ -117,6 +93,7 @@ if (searchParams.get('middleware') === 'true') {
11793 // Represents the main render function for app
11894 async function renderForm ( ) {
11995 journeyEl . innerHTML = '' ;
96+ errorEl . textContent = '' ;
12097
12198 if ( step ?. type !== 'Step' ) {
12299 throw new Error ( 'Expected step to be defined and of type Step' ) ;
@@ -130,6 +107,23 @@ if (searchParams.get('middleware') === 'true') {
130107
131108 const submitForm = ( ) => formEl . requestSubmit ( ) ;
132109
110+ // Handle WebAuthn steps first so we can hide the Submit button while processing,
111+ // auto-submit on success, and show an error on failure.
112+ const webAuthnStep = WebAuthn . getWebAuthnStepType ( step ) ;
113+ if (
114+ webAuthnStep === WebAuthnStepType . Authentication ||
115+ webAuthnStep === WebAuthnStepType . Registration
116+ ) {
117+ const webAuthnSuccess = await webauthnComponent ( journeyEl , step , 0 ) ;
118+ if ( webAuthnSuccess ) {
119+ submitForm ( ) ;
120+ return ;
121+ } else {
122+ errorEl . textContent =
123+ 'WebAuthn failed or was cancelled. Please try again or use a different method.' ;
124+ }
125+ }
126+
133127 const stepRendered =
134128 renderQRCodeStep ( journeyEl , step ) || renderRecoveryCodesStep ( journeyEl , step ) ;
135129
@@ -145,6 +139,52 @@ if (searchParams.get('middleware') === 'true') {
145139 journeyEl . appendChild ( submitBtn ) ;
146140 }
147141
142+ function renderComplete ( ) {
143+ if ( step ?. type !== 'LoginSuccess' ) {
144+ throw new Error ( 'Expected step to be defined and of type LoginSuccess' ) ;
145+ }
146+
147+ const session = step . getSessionToken ( ) ;
148+
149+ console . log ( `Session Token: ${ session || 'none' } ` ) ;
150+
151+ journeyEl . replaceChildren ( ) ;
152+
153+ const completeHeader = document . createElement ( 'h2' ) ;
154+ completeHeader . id = 'completeHeader' ;
155+ completeHeader . innerText = 'Complete' ;
156+ journeyEl . appendChild ( completeHeader ) ;
157+
158+ renderDeleteDevicesSection ( journeyEl , ( ) => deleteWebAuthnDevice ( config ) ) ;
159+
160+ const sessionLabelEl = document . createElement ( 'span' ) ;
161+ sessionLabelEl . id = 'sessionLabel' ;
162+ sessionLabelEl . innerText = 'Session:' ;
163+
164+ const sessionTokenEl = document . createElement ( 'pre' ) ;
165+ sessionTokenEl . id = 'sessionToken' ;
166+ sessionTokenEl . textContent = session || 'none' ;
167+
168+ const logoutBtn = document . createElement ( 'button' ) ;
169+ logoutBtn . type = 'button' ;
170+ logoutBtn . id = 'logoutButton' ;
171+ logoutBtn . innerText = 'Logout' ;
172+
173+ journeyEl . appendChild ( sessionLabelEl ) ;
174+ journeyEl . appendChild ( sessionTokenEl ) ;
175+ journeyEl . appendChild ( logoutBtn ) ;
176+
177+ logoutBtn . addEventListener ( 'click' , async ( ) => {
178+ await journeyClient . terminate ( ) ;
179+
180+ console . log ( 'Logout successful' ) ;
181+
182+ step = await journeyClient . start ( { journey : journeyName } ) ;
183+
184+ renderForm ( ) ;
185+ } ) ;
186+ }
187+
148188 formEl . addEventListener ( 'submit' , async ( event ) => {
149189 event . preventDefault ( ) ;
150190
0 commit comments