From f7061d59702ccd73b1fe0c32e598c6c12d816833 Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Fri, 31 Jul 2026 16:20:43 +0200 Subject: [PATCH 01/11] wip: feat(recognize): add PingOneRecognizeCallback and e2e callback test page --- .../src/index-callback-test.html | 12 + e2e/recognize-app/src/index-callback-test.ts | 226 ++++++++++++++++++ .../src/lib/callbacks/factory.ts | 3 + .../callbacks/ping-one-recognize-callback.ts | 79 ++++++ packages/journey-client/src/types.ts | 1 + .../sdk-types/src/lib/am-callback.types.ts | 1 + 6 files changed, 322 insertions(+) create mode 100644 e2e/recognize-app/src/index-callback-test.html create mode 100644 e2e/recognize-app/src/index-callback-test.ts create mode 100644 packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts diff --git a/e2e/recognize-app/src/index-callback-test.html b/e2e/recognize-app/src/index-callback-test.html new file mode 100644 index 00000000000..050c0e96bb3 --- /dev/null +++ b/e2e/recognize-app/src/index-callback-test.html @@ -0,0 +1,12 @@ + + + + + + Recognize Callback Test | Ping Identity JavaScript SDK + + +
+ + + diff --git a/e2e/recognize-app/src/index-callback-test.ts b/e2e/recognize-app/src/index-callback-test.ts new file mode 100644 index 00000000000..059b4a278a1 --- /dev/null +++ b/e2e/recognize-app/src/index-callback-test.ts @@ -0,0 +1,226 @@ +import { + callbackType, + journey, + NameCallback, + PasswordCallback, + PingOneRecognizeCallback, +} from '@forgerock/journey-client'; +import { recognize } from '@forgerock/recognize'; +import './styles.css'; + + +const appEl = document.getElementById('app') as HTMLDivElement; +appEl.style.cssText = 'display:flex;gap:1.5rem;align-items:flex-start;'; + +const leftEl = document.createElement('div'); +leftEl.style.cssText = 'flex:0 0 400px;min-width:400px;'; +appEl.appendChild(leftEl); + +const rightEl = document.createElement('div'); +rightEl.style.cssText = 'flex:1;height:calc(100vh - 4rem);overflow-y:auto;'; +appEl.appendChild(rightEl); + +console.log('[build] recognize-app loaded'); + +function promptConfig(): Promise<{ wellknown: string; journeyName: string }> { + return new Promise((resolve) => { + const form = document.createElement('form'); + form.style.cssText = 'display:flex;flex-direction:column;gap:0.5rem;'; + form.innerHTML = ` + + + + `; + leftEl.appendChild(form); + form.addEventListener('submit', (e) => { + e.preventDefault(); + const wellknown = (form.querySelector('#wellknown') as HTMLInputElement).value.trim(); + const journeyName = (form.querySelector('#journeyName') as HTMLInputElement).value.trim(); + form.remove(); + resolve({ wellknown, journeyName }); + }); + }); +} + +function log(msg: string) { + console.log(msg); + const p = document.createElement('p'); + p.style.cssText = 'font-family:monospace;font-size:0.85rem;margin:2px 0;'; + if (msg.startsWith('[error]')) p.style.color = 'crimson'; + else if (msg.startsWith('[done]')) p.style.color = 'green'; + else if (msg.startsWith('[recognize]')) p.style.color = '#2563eb'; + else if (msg.startsWith('[step]')) p.style.color = '#7c3aed'; + p.textContent = msg; + rightEl.appendChild(p); +} + +function promptCredentials(): Promise<{ username: string; password: string }> { + return new Promise((resolve) => { + const form = document.createElement('form'); + form.style.cssText = 'display:flex;flex-direction:column;gap:0.5rem;'; + form.innerHTML = ` + + + + `; + leftEl.appendChild(form); + form.addEventListener('submit', (e) => { + e.preventDefault(); + const username = (form.querySelector('#username') as HTMLInputElement).value; + const password = (form.querySelector('#password') as HTMLInputElement).value; + form.remove(); + resolve({ username, password }); + }); + }); +} + +(async () => { + const { wellknown, journeyName } = await promptConfig(); + log('[init] starting journey client...'); + let journeyClient; + try { + journeyClient = await journey({ config: { serverConfig: { wellknown } } }); + } catch (err) { + log(`[error] failed to init journey client: ${err}`); + return; + } + + log('[init] starting journey...'); + let step; + try { + step = await journeyClient.start({ journey: journeyName }); + } catch (err) { + log(`[error] failed to start journey: ${err}`); + return; + } + + while (step.type === 'Step') { + const recognizeCallback = step.callbacks.find( + (cb) => cb.getType() === callbackType.PingOneRecognizeCallback, + ) as PingOneRecognizeCallback | undefined; + + if (recognizeCallback) { + log(`[step] got PingOneRecognizeCallback — op: ${recognizeCallback.getOperationType()}`); + log(`[config] ${JSON.stringify(recognizeCallback.getWebSDKConfig())}`); + + const config = recognizeCallback.getWebSDKConfig(); + const operationType = recognizeCallback.getOperationType(); + + const serviceURL = config.ws.url + .replace(/^wss:\/\//, 'https://') + .replace(/^ws:\/\//, 'http://'); + + log(`[options] webSDKOptions from server: ${JSON.stringify(recognizeCallback.getOptions())}`); + + const client = recognize({ + customer: recognizeCallback.getCustomerName(), + serviceURL, + ...(recognizeCallback.getTransactionData() + ? { transactionData: recognizeCallback.getTransactionData() } + : {}), + ...(recognizeCallback.getOptions() as Record), + }); + + await new Promise((resolve, reject) => { + client.subscribe({ + next: (event) => { + log( + `[recognize] ${event.type}${event.detail ? ': ' + JSON.stringify(event.detail) : ''}`, + ); + }, + error: (err) => { + console.error( + '[recognize] raw error:', + err, + 'constructor:', + err?.constructor?.name, + 'instanceof RecognizeError:', + err instanceof Error, + ); + log( + `[recognize] error: ${JSON.stringify(err)} — code:${err.code} — msg:${err.message} — constructor:${err?.constructor?.name}`, + ); + recognizeCallback.setClientError(err.message); + recognizeCallback.setClientErrorCode(err.code); + resolve(); + }, + complete: (data) => { + log(`[recognize] complete — data: ${JSON.stringify(data)}`); + if (data.jwt) { + recognizeCallback.setSignedJwt(data.jwt); + try { + const payload = JSON.parse(atob(data.jwt.split('.')[1])); + if (payload.sub) { + log(`[recognize] recognizeId from JWT sub: ${payload.sub}`); + recognizeCallback.setRecognizeId(payload.sub); + } + } catch (e) { + log(`[recognize] could not parse JWT sub: ${e}`); + } + } + resolve(); + }, + }); + + const container = document.createElement('div'); + leftEl.appendChild(container); + + client + .init({ + mode: 'mount', + container, + type: operationType === 'ENROLL' ? 'enroll' : 'auth', + username: recognizeCallback.getUsername(), + }) + .then((err) => { + if (err) { + log(`[recognize] init error: ${err}`); + reject(err); + } + }) + .catch((err) => { + log(`[recognize] init threw: ${err}`); + console.error('[recognize] init threw:', err); + reject(err); + }); + }); + + client.dispose(); + } else { + const hasName = step.callbacks.some((cb) => cb.getType() === callbackType.NameCallback); + const hasPassword = step.callbacks.some( + (cb) => cb.getType() === callbackType.PasswordCallback, + ); + + if (hasName || hasPassword) { + log('[step] credentials required'); + const { username, password } = await promptCredentials(); + + if (hasName) { + const cb = step.callbacks.find( + (cb) => cb.getType() === callbackType.NameCallback, + ) as NameCallback; + cb.setName(username); + } + if (hasPassword) { + const cb = step.callbacks.find( + (cb) => cb.getType() === callbackType.PasswordCallback, + ) as PasswordCallback; + cb.setPassword(password); + } + } else { + const types = step.callbacks.map((cb) => cb.getType()).join(', '); + log(`[step] unhandled callbacks: [${types}]`); + break; + } + } + + step = await journeyClient.next(step); + } + + if (step.type === 'LoginSuccess') { + log(`[done] Login successful — session: ${step.getSessionToken() ?? 'none'}`); + } else if (step.type === 'LoginFailure') { + log(`[done] Login failed — ${step.payload.message}`); + } +})(); diff --git a/packages/journey-client/src/lib/callbacks/factory.ts b/packages/journey-client/src/lib/callbacks/factory.ts index f0ebdeec78d..790d9261021 100644 --- a/packages/journey-client/src/lib/callbacks/factory.ts +++ b/packages/journey-client/src/lib/callbacks/factory.ts @@ -19,6 +19,7 @@ import { NameCallback } from './name-callback.js'; import { PasswordCallback } from './password-callback.js'; import { PingOneProtectEvaluationCallback } from './ping-protect-evaluation-callback.js'; import { PingOneProtectInitializeCallback } from './ping-protect-initialize-callback.js'; +import { PingOneRecognizeCallback } from './ping-one-recognize-callback.js'; import { PollingWaitCallback } from './polling-wait-callback.js'; import { ReCaptchaCallback } from './recaptcha-callback.js'; import { ReCaptchaEnterpriseCallback } from './recaptcha-enterprise-callback.js'; @@ -64,6 +65,8 @@ export function createCallback(callback: Callback): BaseCallback { return new PingOneProtectEvaluationCallback(callback); case callbackType.PingOneProtectInitializeCallback: return new PingOneProtectInitializeCallback(callback); + case callbackType.PingOneRecognizeCallback: + return new PingOneRecognizeCallback(callback); case callbackType.PollingWaitCallback: return new PollingWaitCallback(callback); case callbackType.ReCaptchaCallback: diff --git a/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts new file mode 100644 index 00000000000..0f249cd9ad6 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import type { Callback } from '@forgerock/sdk-types'; + +import { BaseCallback } from './base-callback.js'; + +export type PingOneRecognizeOperationType = 'ENROLL' | 'AUTHENTICATE'; + +export interface PingOneRecognizeWebSDKConfig { + customer: { name: string }; + transaction: { data: string }; + username: string; + ws: { url: string }; + [key: string]: unknown; +} + +/** + * @class - Represents a callback used to perform PingOne Recognize (Keyless) biometric operations. + */ +export class PingOneRecognizeCallback extends BaseCallback { + constructor(public override payload: Callback) { + super(payload); + } + + public getOperationType(): PingOneRecognizeOperationType { + return this.getOutputByName('operationType', 'AUTHENTICATE'); + } + + public getServiceURL(): string { + return this.getOutputByName('websocketURL', ''); + } + + public getCustomerName(): string { + return this.getOutputByName('customerName', ''); + } + + public getUsername(): string { + return this.getOutputByName('username', ''); + } + + public getTransactionData(): string { + return this.getOutputByName('transactionData', ''); + } + + public getOptions(): Record { + return this.getOutputByName>('webSDKOptions', {}); + } + + public getWebSDKConfig(): PingOneRecognizeWebSDKConfig { + return { + customer: { name: this.getCustomerName() }, + transaction: { data: this.getTransactionData() }, + username: this.getUsername(), + ws: { url: this.getServiceURL() }, + ...this.getOptions(), + }; + } + + public setSignedJwt(jwt: string): void { + this.setInputValue(jwt, 'IDToken1signedJwt'); + } + + public setRecognizeId(recognizeId: string): void { + this.setInputValue(recognizeId, 'IDToken1recognizeId'); + } + + public setClientError(errorMessage: string): void { + this.setInputValue(errorMessage, 'IDToken1clientError'); + } + + public setClientErrorCode(errorCode: string): void { + this.setInputValue(errorCode, 'IDToken1clientErrorCode'); + } +} diff --git a/packages/journey-client/src/types.ts b/packages/journey-client/src/types.ts index e4802c9db32..0af88cc79d1 100644 --- a/packages/journey-client/src/types.ts +++ b/packages/journey-client/src/types.ts @@ -47,6 +47,7 @@ export * from './lib/callbacks/name-callback.js'; export * from './lib/callbacks/password-callback.js'; export * from './lib/callbacks/ping-protect-evaluation-callback.js'; export * from './lib/callbacks/ping-protect-initialize-callback.js'; +export * from './lib/callbacks/ping-one-recognize-callback.js'; export * from './lib/callbacks/polling-wait-callback.js'; export * from './lib/callbacks/recaptcha-callback.js'; export * from './lib/callbacks/recaptcha-enterprise-callback.js'; diff --git a/packages/sdk-types/src/lib/am-callback.types.ts b/packages/sdk-types/src/lib/am-callback.types.ts index 8ee87effdd6..7fc8cd5c5c3 100644 --- a/packages/sdk-types/src/lib/am-callback.types.ts +++ b/packages/sdk-types/src/lib/am-callback.types.ts @@ -20,6 +20,7 @@ export const callbackType = { PasswordCallback: 'PasswordCallback', PingOneProtectEvaluationCallback: 'PingOneProtectEvaluationCallback', PingOneProtectInitializeCallback: 'PingOneProtectInitializeCallback', + PingOneRecognizeCallback: 'PingOneRecognizeCallback', PollingWaitCallback: 'PollingWaitCallback', ReCaptchaCallback: 'ReCaptchaCallback', ReCaptchaEnterpriseCallback: 'ReCaptchaEnterpriseCallback', From 30a00ce79e24889e1c716320a5687b8c2a695057 Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Tue, 11 Aug 2026 17:50:01 +0200 Subject: [PATCH 02/11] fix(recognize-app): fix type errors after RecognizeError and RecognizeWebComponentEvent type updates --- e2e/recognize-app/src/index-callback-test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/e2e/recognize-app/src/index-callback-test.ts b/e2e/recognize-app/src/index-callback-test.ts index 059b4a278a1..f32103a811a 100644 --- a/e2e/recognize-app/src/index-callback-test.ts +++ b/e2e/recognize-app/src/index-callback-test.ts @@ -8,7 +8,6 @@ import { import { recognize } from '@forgerock/recognize'; import './styles.css'; - const appEl = document.getElementById('app') as HTMLDivElement; appEl.style.cssText = 'display:flex;gap:1.5rem;align-items:flex-start;'; @@ -125,7 +124,7 @@ function promptCredentials(): Promise<{ username: string; password: string }> { client.subscribe({ next: (event) => { log( - `[recognize] ${event.type}${event.detail ? ': ' + JSON.stringify(event.detail) : ''}`, + `[recognize] ${event.type}${'detail' in event ? ': ' + JSON.stringify(event.detail) : ''}`, ); }, error: (err) => { @@ -138,10 +137,10 @@ function promptCredentials(): Promise<{ username: string; password: string }> { err instanceof Error, ); log( - `[recognize] error: ${JSON.stringify(err)} — code:${err.code} — msg:${err.message} — constructor:${err?.constructor?.name}`, + `[recognize] error: ${JSON.stringify(err)} — code:${err.error.code} — msg:${err.error.message} — constructor:${err?.constructor?.name}`, ); - recognizeCallback.setClientError(err.message); - recognizeCallback.setClientErrorCode(err.code); + recognizeCallback.setClientError(err.error.message); + recognizeCallback.setClientErrorCode(String(err.error.code)); resolve(); }, complete: (data) => { From b27829c0677c0378ac31563e5b179c0d511ccca8 Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Wed, 19 Aug 2026 11:44:05 +0200 Subject: [PATCH 03/11] fix(journey-client): use authentication service URL for Recognize callback --- .../src/lib/callbacks/ping-one-recognize-callback.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts index 0f249cd9ad6..61c17cfc590 100644 --- a/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts +++ b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts @@ -32,7 +32,7 @@ export class PingOneRecognizeCallback extends BaseCallback { } public getServiceURL(): string { - return this.getOutputByName('websocketURL', ''); + return this.getOutputByName('authenticationServiceUrl', ''); } public getCustomerName(): string { From 7ff928910f9d030912f01b35306963e93853999e Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Fri, 28 Aug 2026 15:34:24 +0200 Subject: [PATCH 04/11] refactor(journey-client): remove obsolete Recognize SDK config Use PingOneRecognizeCallback getters directly in the Recognize callback app and remove the legacy WebSDKConfig interface and method. --- e2e/recognize-app/src/index-callback-test.ts | 18 +++++++++++------- .../callbacks/ping-one-recognize-callback.ts | 18 ------------------ 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/e2e/recognize-app/src/index-callback-test.ts b/e2e/recognize-app/src/index-callback-test.ts index f32103a811a..e87905ecb6f 100644 --- a/e2e/recognize-app/src/index-callback-test.ts +++ b/e2e/recognize-app/src/index-callback-test.ts @@ -1,3 +1,10 @@ +/* + * Copyright (c) 2026 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + import { callbackType, journey, @@ -6,7 +13,6 @@ import { PingOneRecognizeCallback, } from '@forgerock/journey-client'; import { recognize } from '@forgerock/recognize'; -import './styles.css'; const appEl = document.getElementById('app') as HTMLDivElement; appEl.style.cssText = 'display:flex;gap:1.5rem;align-items:flex-start;'; @@ -100,15 +106,13 @@ function promptCredentials(): Promise<{ username: string; password: string }> { if (recognizeCallback) { log(`[step] got PingOneRecognizeCallback — op: ${recognizeCallback.getOperationType()}`); - log(`[config] ${JSON.stringify(recognizeCallback.getWebSDKConfig())}`); - const config = recognizeCallback.getWebSDKConfig(); const operationType = recognizeCallback.getOperationType(); + const serviceURL = recognizeCallback.getServiceURL(); - const serviceURL = config.ws.url - .replace(/^wss:\/\//, 'https://') - .replace(/^ws:\/\//, 'http://'); - + log(`[config] serviceURL: ${serviceURL}`); + log(`[config] customer: ${recognizeCallback.getCustomerName()}`); + log(`[config] username: ${recognizeCallback.getUsername()}`); log(`[options] webSDKOptions from server: ${JSON.stringify(recognizeCallback.getOptions())}`); const client = recognize({ diff --git a/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts index 61c17cfc590..573863c0ee3 100644 --- a/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts +++ b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts @@ -11,14 +11,6 @@ import { BaseCallback } from './base-callback.js'; export type PingOneRecognizeOperationType = 'ENROLL' | 'AUTHENTICATE'; -export interface PingOneRecognizeWebSDKConfig { - customer: { name: string }; - transaction: { data: string }; - username: string; - ws: { url: string }; - [key: string]: unknown; -} - /** * @class - Represents a callback used to perform PingOne Recognize (Keyless) biometric operations. */ @@ -51,16 +43,6 @@ export class PingOneRecognizeCallback extends BaseCallback { return this.getOutputByName>('webSDKOptions', {}); } - public getWebSDKConfig(): PingOneRecognizeWebSDKConfig { - return { - customer: { name: this.getCustomerName() }, - transaction: { data: this.getTransactionData() }, - username: this.getUsername(), - ws: { url: this.getServiceURL() }, - ...this.getOptions(), - }; - } - public setSignedJwt(jwt: string): void { this.setInputValue(jwt, 'IDToken1signedJwt'); } From 6e0d9b9034d7a0bd8667318d230b5b6cb0335934 Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Mon, 14 Sep 2026 14:53:57 +0200 Subject: [PATCH 05/11] test(journey-client): add unit tests for PingOneRecognizeCallback Cover all getters (operationType, serviceURL, customerName, username, transactionData, options) and setters (signedJwt, recognizeId, clientError, clientErrorCode), including default-value fallbacks for operationType and options. Modeled after the existing ping-protect-evaluation-callback.test.ts pattern. --- .../ping-one-recognize-callback.test.ts | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.test.ts diff --git a/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.test.ts b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.test.ts new file mode 100644 index 00000000000..26a8feed2e2 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.test.ts @@ -0,0 +1,155 @@ +/* + * @forgerock/ping-javascript-sdk + * + * ping-one-recognize-callback.test.ts + * + * Copyright (c) 2026 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { callbackType } from '@forgerock/sdk-types'; +import { vi, describe, it, expect } from 'vitest'; + +import { PingOneRecognizeCallback } from './ping-one-recognize-callback.js'; + +describe('PingOneRecognizeCallback', () => { + it('should be defined', () => { + expect(PingOneRecognizeCallback).toBeDefined(); + }); + + it('should test that the getOperationType method can be called', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [{ name: 'operationType', value: 'ENROLL' }], + }); + const mock = vi.spyOn(callback, 'getOperationType'); + const result = callback.getOperationType(); + expect(mock).toHaveBeenCalled(); + expect(result).toBe('ENROLL'); + }); + + it('should default getOperationType to AUTHENTICATE when output is missing', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [], + }); + expect(callback.getOperationType()).toBe('AUTHENTICATE'); + }); + + it('should test that the getServiceURL method can be called', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [{ name: 'authenticationServiceUrl', value: 'https://recognize.example.com' }], + }); + const mock = vi.spyOn(callback, 'getServiceURL'); + const result = callback.getServiceURL(); + expect(mock).toHaveBeenCalled(); + expect(result).toBe('https://recognize.example.com'); + }); + + it('should test that the getCustomerName method can be called', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [{ name: 'customerName', value: 'acme' }], + }); + const mock = vi.spyOn(callback, 'getCustomerName'); + const result = callback.getCustomerName(); + expect(mock).toHaveBeenCalled(); + expect(result).toBe('acme'); + }); + + it('should test that the getUsername method can be called', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [{ name: 'username', value: 'jdoe' }], + }); + const mock = vi.spyOn(callback, 'getUsername'); + const result = callback.getUsername(); + expect(mock).toHaveBeenCalled(); + expect(result).toBe('jdoe'); + }); + + it('should test that the getTransactionData method can be called', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [{ name: 'transactionData', value: 'txn-123' }], + }); + const mock = vi.spyOn(callback, 'getTransactionData'); + const result = callback.getTransactionData(); + expect(mock).toHaveBeenCalled(); + expect(result).toBe('txn-123'); + }); + + it('should test that the getOptions method can be called', () => { + const options = { requestRecognitionFrame: true }; + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [{ name: 'webSDKOptions', value: options }], + }); + const mock = vi.spyOn(callback, 'getOptions'); + const result = callback.getOptions(); + expect(mock).toHaveBeenCalled(); + expect(result).toEqual(options); + }); + + it('should default getOptions to an empty object when output is missing', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [], + }); + expect(callback.getOptions()).toEqual({}); + }); + + it('should test setSignedJwt method', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [], + input: [{ name: 'IDToken1signedJwt', value: '' }], + }); + const mock = vi.spyOn(callback, 'setSignedJwt'); + callback.setSignedJwt('jwt-value'); + expect(mock).toHaveBeenCalledWith('jwt-value'); + expect(callback.getInputValue('IDToken1signedJwt')).toBe('jwt-value'); + }); + + it('should test setRecognizeId method', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [], + input: [{ name: 'IDToken1recognizeId', value: '' }], + }); + const mock = vi.spyOn(callback, 'setRecognizeId'); + callback.setRecognizeId('recognize-id-123'); + expect(mock).toHaveBeenCalledWith('recognize-id-123'); + expect(callback.getInputValue('IDToken1recognizeId')).toBe('recognize-id-123'); + }); + + it('should test setClientError method', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [], + input: [{ name: 'IDToken1clientError', value: '' }], + }); + // The Keyless SDK's RecognizeError.message is the error code's key name itself + // (see @forgerock/recognize's getRecognizeErrorCodeKey), so this pairs with the + // CORE_FACE_NOT_MATCHING (3004) code used in setClientErrorCode below. + const mock = vi.spyOn(callback, 'setClientError'); + callback.setClientError('CORE_FACE_NOT_MATCHING'); + expect(mock).toHaveBeenCalledWith('CORE_FACE_NOT_MATCHING'); + expect(callback.getInputValue('IDToken1clientError')).toBe('CORE_FACE_NOT_MATCHING'); + }); + + it('should test setClientErrorCode method', () => { + const callback = new PingOneRecognizeCallback({ + type: callbackType.PingOneRecognizeCallback, + output: [], + input: [{ name: 'IDToken1clientErrorCode', value: '' }], + }); + // 3004 is CORE_FACE_NOT_MATCHING from @forgerock/recognize's RECOGNIZE_ERROR_CODE + const mock = vi.spyOn(callback, 'setClientErrorCode'); + callback.setClientErrorCode('3004'); + expect(mock).toHaveBeenCalledWith('3004'); + expect(callback.getInputValue('IDToken1clientErrorCode')).toBe('3004'); + }); +}); From dfc12f9b5c7d37a8b34ecc3a9d511e4596a8287f Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Mon, 14 Sep 2026 16:47:45 +0200 Subject: [PATCH 06/11] test(e2e): add journey e2e tests for PingOneRecognizeCallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a mock TEST_LoginPingRecognize journey to am-mock-api that serves a PingOneRecognizeCallback step and validates the submitted inputs — success requires IDToken1signedJwt whose sub matches IDToken1recognizeId, a non-empty client error is echoed back in the 401 message. Render the step in journey-app with a new ping-one-recognize component that displays all callback getter values and simulates SDK completion (mock JWT + recognizeId derived from its sub) or a client error (CORE_FACE_NOT_MATCHING / 3004 via ?recognizeError=true), then auto-submits, mirroring the ping-protect-* components. Cover both paths in journey-suites with Playwright tests: the success flow asserts rendered outputs, the auto-submitted signedJwt/recognizeId and a completed login; the error flow asserts clientError/clientErrorCode submission and the echoed failure message. The real WASM camera component can't run headless, matching the boundary drawn by the existing Protect e2e tests. --- e2e/am-mock-api/src/app/responses.js | 53 +++++++ e2e/am-mock-api/src/app/routes.auth.js | 51 +++++++ e2e/journey-app/callback-map.ts | 55 ++++---- e2e/journey-app/components/index.ts | 1 + .../components/ping-one-recognize.ts | 84 +++++++++++ e2e/journey-suites/src/recognize.test.ts | 133 ++++++++++++++++++ 6 files changed, 352 insertions(+), 25 deletions(-) create mode 100644 e2e/journey-app/components/ping-one-recognize.ts create mode 100644 e2e/journey-suites/src/recognize.test.ts diff --git a/e2e/am-mock-api/src/app/responses.js b/e2e/am-mock-api/src/app/responses.js index 6f3df3ad2b7..a9265773f7b 100644 --- a/e2e/am-mock-api/src/app/responses.js +++ b/e2e/am-mock-api/src/app/responses.js @@ -395,6 +395,59 @@ export const pingProtectSignalsInitializationOptions = { ], }; +export const pingOneRecognize = { + authId: 'foo', + callbacks: [ + { + type: 'PingOneRecognizeCallback', + output: [ + { + name: 'operationType', + value: 'AUTHENTICATE', + }, + { + name: 'authenticationServiceUrl', + value: 'https://recognize.example.com', + }, + { + name: 'customerName', + value: 'mock-customer', + }, + { + name: 'username', + value: 'sdkuser', + }, + { + name: 'transactionData', + value: 'mock-transaction-data', + }, + { + name: 'webSDKOptions', + value: { requestRecognitionFrame: true }, + }, + ], + input: [ + { + name: 'IDToken1signedJwt', + value: '', + }, + { + name: 'IDToken1recognizeId', + value: '', + }, + { + name: 'IDToken1clientError', + value: '', + }, + { + name: 'IDToken1clientErrorCode', + value: '', + }, + ], + }, + ], +}; + export const choiceCallback = { authId: 'foo', callbacks: [ diff --git a/e2e/am-mock-api/src/app/routes.auth.js b/e2e/am-mock-api/src/app/routes.auth.js index 046d41c8e43..5616e5d4569 100644 --- a/e2e/am-mock-api/src/app/routes.auth.js +++ b/e2e/am-mock-api/src/app/routes.auth.js @@ -29,6 +29,7 @@ import { pingProtectEvaluate, pingProtectInitialize, pingProtectSignalsInitializationOptions, + pingOneRecognize, redirectCallback, redirectCallbackSaml, requestDeviceProfile, @@ -61,6 +62,19 @@ import wait from './wait.js'; console.log(`Your user password from 'env.config' file: ${USERS[0].pw}`); +/** + * Decodes the payload segment of an unsigned JWT and returns its `sub` claim, + * or null if the token can't be parsed. + */ +function getJwtSub(jwt) { + try { + const [, payload] = jwt.split('.'); + return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/'))).sub ?? null; + } catch { + return null; + } +} + export const baz = { canWithdraw: false, }; @@ -92,6 +106,8 @@ export default function (app) { req.query.authIndexValue === 'SAMLFailure' ) { res.json(nameCallback); + } else if (req.query.authIndexValue === 'TEST_LoginPingRecognize') { + res.json({ ...initialBasicLogin, authId: 'recognize-journey-login' }); } else if (req.query.authIndexValue === 'TEST_LoginPingProtect') { res.json({ ...pingProtectInitialize, authId: 'protect-journey-init' }); } else if (req.query.authIndexValue === 'TEST_LoginPingProtectSignalsOptions') { @@ -365,6 +381,41 @@ export default function (app) { } else { res.status(401).json(authFail); } + } else if ( + req.query.authIndexValue === 'TEST_LoginPingRecognize' || + req.body.authId?.startsWith('recognize-journey') + ) { + const recognizeCb = req.body.callbacks.find((cb) => cb.type === 'PingOneRecognizeCallback'); + const passwordCb = req.body.callbacks.find((cb) => cb.type === 'PasswordCallback'); + + if (recognizeCb) { + const inputValue = (name) => { + const input = recognizeCb.input?.find((x) => x.name === name); + return input ? String(input.value ?? '') : ''; + }; + const signedJwt = inputValue('IDToken1signedJwt'); + const recognizeId = inputValue('IDToken1recognizeId'); + const clientError = inputValue('IDToken1clientError'); + const clientErrorCode = inputValue('IDToken1clientErrorCode'); + + if (clientError) { + res.status(401).json({ + ...authFail, + message: `Recognize client error: ${clientError} (${clientErrorCode})`, + }); + } else if (signedJwt && recognizeId && getJwtSub(signedJwt) === recognizeId) { + res.cookie('iPlanetDirectoryPro', 'recognize-session-' + Date.now(), { + domain: 'localhost', + }); + res.json(authSuccess); + } else { + res.status(401).json(authFail); + } + } else if (passwordCb && passwordCb.input[0].value === USERS[0].pw) { + res.json({ ...pingOneRecognize, authId: 'recognize-journey-recognize' }); + } else { + res.status(401).json(authFail); + } } else if (req.body.callbacks.find((cb) => cb.type === 'PasswordCallback')) { const pwCb = req.body.callbacks.find((cb) => cb.type === 'PasswordCallback'); if (pwCb.input[0].value !== USERS[0].pw) { diff --git a/e2e/journey-app/callback-map.ts b/e2e/journey-app/callback-map.ts index c43c47bd7ab..d73e6f10280 100644 --- a/e2e/journey-app/callback-map.ts +++ b/e2e/journey-app/callback-map.ts @@ -1,34 +1,10 @@ /* - * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { - attributeInputComponent, - choiceComponent, - confirmationComponent, - deviceProfileComponent, - hiddenValueComponent, - kbaCreateComponent, - metadataComponent, - passwordComponent, - pingProtectEvaluationComponent, - pingProtectInitializeComponent, - pollingWaitComponent, - recaptchaComponent, - recaptchaEnterpriseComponent, - redirectComponent, - selectIdpComponent, - suspendedTextOutputComponent, - termsAndConditionsComponent, - textInputComponent, - textOutputComponent, - validatedPasswordComponent, - validatedUsernameComponent, -} from './components/index.js'; - import type { AttributeInputCallback, BaseCallback, @@ -42,6 +18,7 @@ import type { PasswordCallback, PingOneProtectEvaluationCallback, PingOneProtectInitializeCallback, + PingOneRecognizeCallback, PollingWaitCallback, ReCaptchaCallback, ReCaptchaEnterpriseCallback, @@ -55,6 +32,31 @@ import type { ValidatedCreateUsernameCallback, } from '@forgerock/journey-client/types'; +import { + attributeInputComponent, + choiceComponent, + confirmationComponent, + deviceProfileComponent, + hiddenValueComponent, + kbaCreateComponent, + metadataComponent, + passwordComponent, + pingProtectEvaluationComponent, + pingProtectInitializeComponent, + pingOneRecognizeComponent, + pollingWaitComponent, + recaptchaComponent, + recaptchaEnterpriseComponent, + redirectComponent, + selectIdpComponent, + suspendedTextOutputComponent, + termsAndConditionsComponent, + textInputComponent, + textOutputComponent, + validatedPasswordComponent, + validatedUsernameComponent, +} from './components/index.js'; + /** * Renders a callback component based on its type * @param journeyEl - The container element to append the component to @@ -118,6 +120,9 @@ export function renderCallback( onSubmit, ); break; + case 'PingOneRecognizeCallback': + pingOneRecognizeComponent(journeyEl, callback as PingOneRecognizeCallback, idx, onSubmit); + break; case 'PollingWaitCallback': pollingWaitComponent(journeyEl, callback as PollingWaitCallback, idx); break; diff --git a/e2e/journey-app/components/index.ts b/e2e/journey-app/components/index.ts index 425c8560be7..8e97e9ebe68 100644 --- a/e2e/journey-app/components/index.ts +++ b/e2e/journey-app/components/index.ts @@ -22,6 +22,7 @@ export { default as metadataComponent } from './metadata.js'; export { default as passwordComponent } from './password.js'; export { default as pingProtectEvaluationComponent } from './ping-protect-evaluation.js'; export { default as pingProtectInitializeComponent } from './ping-protect-initialize.js'; +export { default as pingOneRecognizeComponent } from './ping-one-recognize.js'; export { default as pollingWaitComponent } from './polling-wait.js'; export { default as recaptchaComponent } from './recaptcha.js'; export { default as recaptchaEnterpriseComponent } from './recaptcha-enterprise.js'; diff --git a/e2e/journey-app/components/ping-one-recognize.ts b/e2e/journey-app/components/ping-one-recognize.ts new file mode 100644 index 00000000000..cd0e53ed087 --- /dev/null +++ b/e2e/journey-app/components/ping-one-recognize.ts @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import type { PingOneRecognizeCallback } from '@forgerock/journey-client/types'; + +/** + * PingOne Recognize Component + * + * The real @forgerock/recognize WASM camera component can't run headless, + * so this simulates its completion: it renders the callback's server-provided + * configuration (via the getters), then either produces a signed JWT + + * recognizeId or a client error — controlled by `?recognizeError=true` — + * and auto-submits the step. + */ +export default function pingOneRecognizeComponent( + journeyEl: HTMLDivElement, + callback: PingOneRecognizeCallback, + idx: number, + onSubmit?: () => void, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const message = document.createElement('p'); + + message.id = collectorKey; + message.innerText = 'Starting PingOne Recognize...'; + + journeyEl?.appendChild(message); + + // Show the values the callback exposes from the server's outputs + const config = document.createElement('pre'); + config.id = 'recognizeConfig'; + config.innerText = JSON.stringify( + { + operationType: callback.getOperationType(), + serviceURL: callback.getServiceURL(), + customerName: callback.getCustomerName(), + username: callback.getUsername(), + transactionData: callback.getTransactionData(), + webSDKOptions: callback.getOptions(), + }, + null, + 2, + ); + journeyEl?.appendChild(config); + + // Simulate the Recognize web component completing (or failing) + setTimeout(async () => { + const recognizeError = new URLSearchParams(window.location.search).get('recognizeError'); + + if (recognizeError) { + // Pairs with 3004 (CORE_FACE_NOT_MATCHING) from @forgerock/recognize + console.log('Recognize error simulated'); + callback.setClientError('CORE_FACE_NOT_MATCHING'); + callback.setClientErrorCode('3004'); + message.innerText = 'Recognize failed: CORE_FACE_NOT_MATCHING'; + message.style.color = 'red'; + } else { + console.log('Recognize data collected successfully'); + const signedJwt = [ + window.btoa('{"alg":"none"}'), + window.btoa('{"sub":"mock-recognize-id"}'), + window.btoa('signature'), + ].join('.'); + callback.setSignedJwt(signedJwt); + try { + const payload = JSON.parse(atob(signedJwt.split('.')[1])); + if (payload.sub) { + callback.setRecognizeId(payload.sub); + } + } catch (e) { + console.error('Could not parse mock JWT sub:', e); + } + message.innerText = 'Recognize completed successfully!'; + message.style.color = 'green'; + } + + if (onSubmit) { + setTimeout(() => onSubmit(), 500); + } + }, 100); +} diff --git a/e2e/journey-suites/src/recognize.test.ts b/e2e/journey-suites/src/recognize.test.ts new file mode 100644 index 00000000000..4ac69191c4a --- /dev/null +++ b/e2e/journey-suites/src/recognize.test.ts @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2026 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { expect, test } from '@playwright/test'; +import { asyncEvents } from './utils/async-events.js'; +import { username, password } from './utils/demo-user.js'; + +interface RecognizeInputCapture { + signedJwt: string | null; + recognizeId: string | null; + clientError: string | null; + clientErrorCode: string | null; +} + +function captureRecognizeInputs(page: import('@playwright/test').Page) { + const captured: RecognizeInputCapture[] = []; + + page.on('request', (request) => { + if (request.url().includes('/authenticate') && request.method() === 'POST') { + try { + const postData = request.postData(); + if (postData) { + const body = JSON.parse(postData); + const callbacks = body.callbacks || []; + for (const callback of callbacks) { + if (callback.type === 'PingOneRecognizeCallback') { + const inputs = callback.input || []; + const value = (name: string) => + inputs.find((input: { name: string }) => input.name === name)?.value ?? null; + captured.push({ + signedJwt: value('IDToken1signedJwt'), + recognizeId: value('IDToken1recognizeId'), + clientError: value('IDToken1clientError'), + clientErrorCode: value('IDToken1clientErrorCode'), + }); + } + } + } + } catch { + // Ignore parsing errors + } + } + }); + + return captured; +} + +test('Test PingOne Recognize journey flow', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + const messageArray: string[] = []; + const captured = captureRecognizeInputs(page); + + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + await navigate('/?journey=TEST_LoginPingRecognize&clientId=basic'); + + await expect(page.getByLabel('User Name')).toBeVisible({ timeout: 15000 }); + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + // The callback component should render the server-provided outputs + await expect(page.locator('#recognizeConfig')).toBeVisible({ timeout: 10000 }); + await expect(page.locator('#recognizeConfig')).toContainText('"operationType": "AUTHENTICATE"'); + await expect(page.locator('#recognizeConfig')).toContainText('https://recognize.example.com'); + await expect(page.locator('#recognizeConfig')).toContainText('mock-customer'); + await expect(page.locator('#recognizeConfig')).toContainText('"sdkuser"'); + await expect(page.locator('#recognizeConfig')).toContainText('mock-transaction-data'); + await expect(page.locator('#recognizeConfig')).toContainText('"requestRecognitionFrame": true'); + + // Simulated Recognize completion sets the JWT and auto-submits + await expect(page.getByText('Recognize completed successfully!')).toBeVisible({ + timeout: 10000, + }); + + // Wait for the recognize callback to auto-submit and complete + await page.waitForResponse((response) => response.url().includes('/authenticate')); + + await expect(page.getByText('Complete')).toBeVisible({ timeout: 15000 }); + + // Verify the signed JWT and recognizeId were submitted to the server + expect(captured.length).toBeGreaterThan(0); + const lastSubmit = captured[captured.length - 1]; + expect(lastSubmit.signedJwt).toBeTruthy(); + expect(lastSubmit.signedJwt).toContain('.'); + expect(lastSubmit.recognizeId).toBe('mock-recognize-id'); + expect(lastSubmit.clientError).toBe(''); + + // Verify the recognize SDK flow through console logs + expect(messageArray.some((msg) => msg.includes('Recognize data collected successfully'))).toBe( + true, + ); +}); + +test('Test PingOne Recognize journey flow with client error', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + const captured = captureRecognizeInputs(page); + + await navigate('/?journey=TEST_LoginPingRecognize&clientId=basic&recognizeError=true'); + + await expect(page.getByLabel('User Name')).toBeVisible({ timeout: 15000 }); + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + // The component simulates a Recognize failure and auto-submits the client error + await expect(page.getByText('Recognize failed: CORE_FACE_NOT_MATCHING')).toBeVisible({ + timeout: 10000, + }); + + await page.waitForResponse((response) => response.url().includes('/authenticate')); + + // The mock echoes the submitted client error in the 401 failure message + const errorMessage = page.locator('#errorMessage'); + await expect(errorMessage).toBeVisible({ timeout: 15000 }); + await expect(errorMessage).toContainText('CORE_FACE_NOT_MATCHING'); + await expect(errorMessage).toContainText('3004'); + + // Verify the client error was submitted and no JWT was sent + expect(captured.length).toBeGreaterThan(0); + const lastSubmit = captured[captured.length - 1]; + expect(lastSubmit.clientError).toBe('CORE_FACE_NOT_MATCHING'); + expect(lastSubmit.clientErrorCode).toBe('3004'); + expect(lastSubmit.signedJwt).toBe(''); + expect(lastSubmit.recognizeId).toBe(''); +}); From 76aa265b0252f1f6ae2f249fd7934581d0229dd1 Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Sun, 20 Sep 2026 22:19:24 +0200 Subject: [PATCH 07/11] chore(changeset): add changeset for recognize callback support --- .changeset/recognize-callback-support.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/recognize-callback-support.md diff --git a/.changeset/recognize-callback-support.md b/.changeset/recognize-callback-support.md new file mode 100644 index 00000000000..3221162d023 --- /dev/null +++ b/.changeset/recognize-callback-support.md @@ -0,0 +1,5 @@ +--- +'@forgerock/journey-client': minor +--- + +Add support for PingOne Recognize enrollment and authentication callbacks From 310332c775e156221b26a01fd53917eb62589ca8 Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Sun, 20 Sep 2026 22:29:51 +0200 Subject: [PATCH 08/11] chore(recognize): make recognize package releaseable by changesets --- .changeset/config.json | 1 - packages/recognize/package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index 28ecbd23414..791171e6046 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -17,7 +17,6 @@ "@forgerock/pingone-scripts", "@forgerock/device-client-app", "@forgerock/davinci-app", - "@forgerock/recognize", "@forgerock/davinci-suites", "@forgerock/api-report", "@forgerock/interface-mapping-validator", diff --git a/packages/recognize/package.json b/packages/recognize/package.json index a5ae62f569b..060ae20e617 100644 --- a/packages/recognize/package.json +++ b/packages/recognize/package.json @@ -1,7 +1,6 @@ { "name": "@forgerock/recognize", "version": "0.0.1", - "private": true, "repository": { "type": "git", "url": "git+https://github.com/ForgeRock/ping-javascript-sdk.git", From 952e68289db64d16d358296ccdd029e851d3d053 Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Mon, 21 Sep 2026 17:37:06 +0200 Subject: [PATCH 09/11] refactor(journey-client): fix import ordering in recognize callback files --- packages/journey-client/src/lib/callbacks/factory.ts | 2 +- .../src/lib/callbacks/ping-one-recognize-callback.test.ts | 2 +- .../src/lib/callbacks/ping-one-recognize-callback.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/journey-client/src/lib/callbacks/factory.ts b/packages/journey-client/src/lib/callbacks/factory.ts index 790d9261021..580f9896ef2 100644 --- a/packages/journey-client/src/lib/callbacks/factory.ts +++ b/packages/journey-client/src/lib/callbacks/factory.ts @@ -17,9 +17,9 @@ import { KbaCreateCallback } from './kba-create-callback.js'; import { MetadataCallback } from './metadata-callback.js'; import { NameCallback } from './name-callback.js'; import { PasswordCallback } from './password-callback.js'; +import { PingOneRecognizeCallback } from './ping-one-recognize-callback.js'; import { PingOneProtectEvaluationCallback } from './ping-protect-evaluation-callback.js'; import { PingOneProtectInitializeCallback } from './ping-protect-initialize-callback.js'; -import { PingOneRecognizeCallback } from './ping-one-recognize-callback.js'; import { PollingWaitCallback } from './polling-wait-callback.js'; import { ReCaptchaCallback } from './recaptcha-callback.js'; import { ReCaptchaEnterpriseCallback } from './recaptcha-enterprise-callback.js'; diff --git a/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.test.ts b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.test.ts index 26a8feed2e2..5cc64a24bb9 100644 --- a/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.test.ts +++ b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.test.ts @@ -9,7 +9,7 @@ */ import { callbackType } from '@forgerock/sdk-types'; -import { vi, describe, it, expect } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { PingOneRecognizeCallback } from './ping-one-recognize-callback.js'; diff --git a/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts index 573863c0ee3..44d4c5b3897 100644 --- a/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts +++ b/packages/journey-client/src/lib/callbacks/ping-one-recognize-callback.ts @@ -5,10 +5,10 @@ * of the MIT license. See the LICENSE file for details. */ -import type { Callback } from '@forgerock/sdk-types'; - import { BaseCallback } from './base-callback.js'; +import type { Callback } from '@forgerock/sdk-types'; + export type PingOneRecognizeOperationType = 'ENROLL' | 'AUTHENTICATE'; /** From 92ee54876bf918ea60b8b04f624c056af6d57a83 Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Mon, 21 Sep 2026 17:47:06 +0200 Subject: [PATCH 10/11] refactor(e2e): use type-only import for callback types in recognize app --- e2e/recognize-app/src/index-callback-test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/e2e/recognize-app/src/index-callback-test.ts b/e2e/recognize-app/src/index-callback-test.ts index e87905ecb6f..c5cbb9e7026 100644 --- a/e2e/recognize-app/src/index-callback-test.ts +++ b/e2e/recognize-app/src/index-callback-test.ts @@ -5,9 +5,8 @@ * of the MIT license. See the LICENSE file for details. */ -import { - callbackType, - journey, +import { callbackType, journey } from '@forgerock/journey-client'; +import type { NameCallback, PasswordCallback, PingOneRecognizeCallback, From b4b31140fb185bd0a33dddf2c9d3c079627bda4b Mon Sep 17 00:00:00 2001 From: Eugenio Bettini Date: Mon, 21 Sep 2026 17:59:42 +0200 Subject: [PATCH 11/11] fix(e2e): fix import sorting and dependency order lint errors --- e2e/journey-app/callback-map.ts | 50 ++++++++++---------- e2e/journey-app/package.json | 4 +- e2e/recognize-app/src/index-callback-test.ts | 3 +- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/e2e/journey-app/callback-map.ts b/e2e/journey-app/callback-map.ts index d73e6f10280..42fc183535c 100644 --- a/e2e/journey-app/callback-map.ts +++ b/e2e/journey-app/callback-map.ts @@ -5,6 +5,31 @@ * of the MIT license. See the LICENSE file for details. */ +import { + attributeInputComponent, + choiceComponent, + confirmationComponent, + deviceProfileComponent, + hiddenValueComponent, + kbaCreateComponent, + metadataComponent, + passwordComponent, + pingOneRecognizeComponent, + pingProtectEvaluationComponent, + pingProtectInitializeComponent, + pollingWaitComponent, + recaptchaComponent, + recaptchaEnterpriseComponent, + redirectComponent, + selectIdpComponent, + suspendedTextOutputComponent, + termsAndConditionsComponent, + textInputComponent, + textOutputComponent, + validatedPasswordComponent, + validatedUsernameComponent, +} from './components/index.js'; + import type { AttributeInputCallback, BaseCallback, @@ -32,31 +57,6 @@ import type { ValidatedCreateUsernameCallback, } from '@forgerock/journey-client/types'; -import { - attributeInputComponent, - choiceComponent, - confirmationComponent, - deviceProfileComponent, - hiddenValueComponent, - kbaCreateComponent, - metadataComponent, - passwordComponent, - pingProtectEvaluationComponent, - pingProtectInitializeComponent, - pingOneRecognizeComponent, - pollingWaitComponent, - recaptchaComponent, - recaptchaEnterpriseComponent, - redirectComponent, - selectIdpComponent, - suspendedTextOutputComponent, - termsAndConditionsComponent, - textInputComponent, - textOutputComponent, - validatedPasswordComponent, - validatedUsernameComponent, -} from './components/index.js'; - /** * Renders a callback component based on its type * @param journeyEl - The container element to append the component to diff --git a/e2e/journey-app/package.json b/e2e/journey-app/package.json index 7cc9e674855..917d423425d 100644 --- a/e2e/journey-app/package.json +++ b/e2e/journey-app/package.json @@ -11,11 +11,11 @@ "serve": "pnpm nx nxServe" }, "dependencies": { + "@forgerock/device-client": "workspace:*", "@forgerock/journey-client": "workspace:*", "@forgerock/oidc-client": "workspace:*", "@forgerock/protect": "workspace:*", - "@forgerock/sdk-logger": "workspace:*", - "@forgerock/device-client": "workspace:*" + "@forgerock/sdk-logger": "workspace:*" }, "nx": { "tags": ["scope:e2e"] diff --git a/e2e/recognize-app/src/index-callback-test.ts b/e2e/recognize-app/src/index-callback-test.ts index c5cbb9e7026..603b9eba30d 100644 --- a/e2e/recognize-app/src/index-callback-test.ts +++ b/e2e/recognize-app/src/index-callback-test.ts @@ -6,12 +6,13 @@ */ import { callbackType, journey } from '@forgerock/journey-client'; +import { recognize } from '@forgerock/recognize'; + import type { NameCallback, PasswordCallback, PingOneRecognizeCallback, } from '@forgerock/journey-client'; -import { recognize } from '@forgerock/recognize'; const appEl = document.getElementById('app') as HTMLDivElement; appEl.style.cssText = 'display:flex;gap:1.5rem;align-items:flex-start;';