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
11- import type { JourneyClient , RequestMiddleware } from '@forgerock/journey-client/types' ;
12+ import type {
13+ JourneyClient ,
14+ JourneyClientConfig ,
15+ RequestMiddleware ,
16+ } from '@forgerock/journey-client/types' ;
1217
1318import { renderCallbacks } from './callback-map.js' ;
19+ import { renderDeleteDevicesSection } from './components/webauthn-devices.js' ;
1420import { renderQRCodeStep } from './components/qr-code.js' ;
1521import { renderRecoveryCodesStep } from './components/recovery-codes.js' ;
22+ import {
23+ deleteAllDevices ,
24+ deleteDevicesInSession ,
25+ storeDevicesBeforeSession ,
26+ } from './services/delete-webauthn-devices.js' ;
27+ import { webauthnComponent } from './components/webauthn.js' ;
1628import { serverConfigs } from './server-configs.js' ;
1729
1830const qs = window . location . search ;
@@ -61,7 +73,12 @@ if (searchParams.get('middleware') === 'true') {
6173
6274 let journeyClient : JourneyClient ;
6375 try {
64- journeyClient = await journey ( { config : config , requestMiddleware } ) ;
76+ const journeyConfig : JourneyClientConfig = {
77+ serverConfig : {
78+ wellknown : config . serverConfig . wellknown ,
79+ } ,
80+ } ;
81+ journeyClient = await journey ( { config : journeyConfig , requestMiddleware } ) ;
6582 } catch ( error ) {
6683 const message = error instanceof Error ? error . message : 'Unknown error' ;
6784 console . error ( 'Failed to initialize journey client:' , message ) ;
@@ -70,34 +87,6 @@ if (searchParams.get('middleware') === 'true') {
7087 }
7188 let step = await journeyClient . start ( { journey : journeyName } ) ;
7289
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-
10190 function renderError ( ) {
10291 if ( step ?. type !== 'LoginFailure' ) {
10392 throw new Error ( 'Expected step to be defined and of type LoginFailure' ) ;
@@ -117,6 +106,7 @@ if (searchParams.get('middleware') === 'true') {
117106 // Represents the main render function for app
118107 async function renderForm ( ) {
119108 journeyEl . innerHTML = '' ;
109+ errorEl . textContent = '' ;
120110
121111 if ( step ?. type !== 'Step' ) {
122112 throw new Error ( 'Expected step to be defined and of type Step' ) ;
@@ -130,6 +120,23 @@ if (searchParams.get('middleware') === 'true') {
130120
131121 const submitForm = ( ) => formEl . requestSubmit ( ) ;
132122
123+ // Handle WebAuthn steps first so we can hide the Submit button while processing,
124+ // auto-submit on success, and show an error on failure.
125+ const webAuthnStep = WebAuthn . getWebAuthnStepType ( step ) ;
126+ const isWebAuthn =
127+ webAuthnStep === WebAuthnStepType . Authentication ||
128+ webAuthnStep === WebAuthnStepType . Registration ;
129+ if ( isWebAuthn ) {
130+ const webauthnSucceeded = await webauthnComponent ( journeyEl , step , 0 ) ;
131+ if ( webauthnSucceeded ) {
132+ submitForm ( ) ;
133+ return ;
134+ } else {
135+ errorEl . textContent =
136+ 'WebAuthn failed or was cancelled. Please try again or use a different method.' ;
137+ }
138+ }
139+
133140 const stepRendered =
134141 renderQRCodeStep ( journeyEl , step ) || renderRecoveryCodesStep ( journeyEl , step ) ;
135142
@@ -145,6 +152,57 @@ if (searchParams.get('middleware') === 'true') {
145152 journeyEl . appendChild ( submitBtn ) ;
146153 }
147154
155+ function renderComplete ( ) {
156+ if ( step ?. type !== 'LoginSuccess' ) {
157+ throw new Error ( 'Expected step to be defined and of type LoginSuccess' ) ;
158+ }
159+
160+ const session = step . getSessionToken ( ) ;
161+
162+ console . log ( `Session Token: ${ session || 'none' } ` ) ;
163+
164+ journeyEl . replaceChildren ( ) ;
165+
166+ const completeHeader = document . createElement ( 'h2' ) ;
167+ completeHeader . id = 'completeHeader' ;
168+ completeHeader . innerText = 'Complete' ;
169+ journeyEl . appendChild ( completeHeader ) ;
170+
171+ renderDeleteDevicesSection (
172+ journeyEl ,
173+ ( ) => storeDevicesBeforeSession ( config ) ,
174+ ( ) => deleteDevicesInSession ( config ) ,
175+ ( ) => deleteAllDevices ( config ) ,
176+ ) ;
177+
178+ const sessionLabelEl = document . createElement ( 'span' ) ;
179+ sessionLabelEl . id = 'sessionLabel' ;
180+ sessionLabelEl . innerText = 'Session:' ;
181+
182+ const sessionTokenEl = document . createElement ( 'pre' ) ;
183+ sessionTokenEl . id = 'sessionToken' ;
184+ sessionTokenEl . textContent = session || 'none' ;
185+
186+ const logoutBtn = document . createElement ( 'button' ) ;
187+ logoutBtn . type = 'button' ;
188+ logoutBtn . id = 'logoutButton' ;
189+ logoutBtn . innerText = 'Logout' ;
190+
191+ journeyEl . appendChild ( sessionLabelEl ) ;
192+ journeyEl . appendChild ( sessionTokenEl ) ;
193+ journeyEl . appendChild ( logoutBtn ) ;
194+
195+ logoutBtn . addEventListener ( 'click' , async ( ) => {
196+ await journeyClient . terminate ( ) ;
197+
198+ console . log ( 'Logout successful' ) ;
199+
200+ step = await journeyClient . start ( { journey : journeyName } ) ;
201+
202+ renderForm ( ) ;
203+ } ) ;
204+ }
205+
148206 formEl . addEventListener ( 'submit' , async ( event ) => {
149207 event . preventDefault ( ) ;
150208
0 commit comments