Skip to content

Commit ac7fe33

Browse files
committed
chore: updates
1 parent ed1cb7f commit ac7fe33

10 files changed

Lines changed: 346 additions & 54 deletions

File tree

e2e/journey-app/components/device-profile.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ export default function deviceProfileComponent(
4848

4949
// Auto-submit the form after successful collection
5050
setTimeout(() => {
51-
const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
52-
if (submitButton) {
53-
submitButton.click();
51+
const form = document.getElementById('form') as HTMLFormElement;
52+
if (form) {
53+
form.requestSubmit();
5454
}
5555
}, 500);
5656
} catch (error) {

e2e/journey-app/components/ping-protect-evaluation.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ export default function pingProtectEvaluationComponent(
5858

5959
// Auto-submit the form after successful data collection
6060
setTimeout(() => {
61-
const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
62-
if (submitButton) {
63-
submitButton.click();
61+
const form = document.getElementById('form') as HTMLFormElement;
62+
if (form) {
63+
form.requestSubmit();
6464
}
6565
}, 500);
6666
} catch (error) {

e2e/journey-app/components/ping-protect-initialize.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ export default function pingProtectInitializeComponent(
7676

7777
// Auto-submit the form after successful initialization
7878
setTimeout(() => {
79-
const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
80-
if (submitButton) {
81-
submitButton.click();
79+
const form = document.getElementById('form') as HTMLFormElement;
80+
if (form) {
81+
form.requestSubmit();
8282
}
8383
}, 500);
8484
} catch (error) {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import { QRCode } from '@forgerock/journey-client/qr-code';
8+
import type { JourneyStep, ConfirmationCallback } from '@forgerock/journey-client/types';
9+
10+
export function renderQRCodeStep(journeyEl: HTMLDivElement, step: JourneyStep): boolean {
11+
if (!QRCode.isQRCodeStep(step)) {
12+
return false;
13+
}
14+
15+
const qrCodeData = QRCode.getQRCodeData(step);
16+
17+
console.log('QR Code step detected via QRCode module');
18+
console.log('QR Code data:', JSON.stringify(qrCodeData));
19+
20+
const container = document.createElement('div');
21+
container.id = 'qr-code-container';
22+
23+
const message = document.createElement('p');
24+
message.id = 'qr-code-message';
25+
message.innerText = qrCodeData.message || 'Scan the QR code below';
26+
container.appendChild(message);
27+
28+
const uriDisplay = document.createElement('div');
29+
uriDisplay.id = 'qr-code-uri';
30+
uriDisplay.style.cssText = `
31+
padding: 10px;
32+
background-color: #f5f5f5;
33+
border: 1px solid #ddd;
34+
border-radius: 4px;
35+
font-family: monospace;
36+
font-size: 12px;
37+
word-break: break-all;
38+
margin: 10px 0;
39+
`;
40+
uriDisplay.innerText = qrCodeData.uri;
41+
container.appendChild(uriDisplay);
42+
43+
const useType = document.createElement('p');
44+
useType.id = 'qr-code-use-type';
45+
useType.innerText = `Type: ${qrCodeData.use}`;
46+
useType.style.color = '#666';
47+
container.appendChild(useType);
48+
49+
const confirmationCallbacks =
50+
step.getCallbacksOfType<ConfirmationCallback>('ConfirmationCallback');
51+
52+
if (confirmationCallbacks.length > 0) {
53+
const confirmCb = confirmationCallbacks[0];
54+
const options = confirmCb.getOptions();
55+
56+
const optionsContainer = document.createElement('div');
57+
optionsContainer.style.marginTop = '10px';
58+
59+
options.forEach((option, index) => {
60+
const label = document.createElement('label');
61+
label.style.display = 'block';
62+
label.style.marginBottom = '5px';
63+
64+
const radio = document.createElement('input');
65+
radio.type = 'radio';
66+
radio.name = 'qr-confirmation';
67+
radio.value = String(index);
68+
radio.checked = index === confirmCb.getDefaultOption();
69+
radio.addEventListener('change', () => {
70+
confirmCb.setOptionIndex(index);
71+
});
72+
73+
if (index === confirmCb.getDefaultOption()) {
74+
confirmCb.setOptionIndex(index);
75+
}
76+
77+
label.appendChild(radio);
78+
label.appendChild(document.createTextNode(` ${option}`));
79+
optionsContainer.appendChild(label);
80+
});
81+
82+
container.appendChild(optionsContainer);
83+
}
84+
85+
journeyEl.appendChild(container);
86+
87+
return true;
88+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import { RecoveryCodes } from '@forgerock/journey-client/recovery-codes';
8+
import type { JourneyStep, ConfirmationCallback } from '@forgerock/journey-client/types';
9+
10+
export function renderRecoveryCodesStep(journeyEl: HTMLDivElement, step: JourneyStep): boolean {
11+
if (!RecoveryCodes.isDisplayStep(step)) {
12+
return false;
13+
}
14+
15+
const codes = RecoveryCodes.getCodes(step);
16+
const deviceName = RecoveryCodes.getDeviceName(step);
17+
18+
console.log('Recovery Codes step detected via RecoveryCodes module');
19+
console.log('Recovery codes:', JSON.stringify(codes));
20+
console.log('Device name:', deviceName);
21+
22+
const container = document.createElement('div');
23+
container.id = 'recovery-codes-container';
24+
25+
const header = document.createElement('h3');
26+
header.id = 'recovery-codes-header';
27+
header.innerText = 'Your Recovery Codes';
28+
container.appendChild(header);
29+
30+
const instruction = document.createElement('p');
31+
instruction.innerText =
32+
'You must make a copy of these recovery codes. They cannot be displayed again.';
33+
instruction.style.color = '#666';
34+
container.appendChild(instruction);
35+
36+
const codesContainer = document.createElement('div');
37+
codesContainer.id = 'recovery-codes-list';
38+
codesContainer.style.cssText = `
39+
padding: 15px;
40+
background-color: #f5f5f5;
41+
border: 1px solid #ddd;
42+
border-radius: 4px;
43+
font-family: monospace;
44+
font-size: 14px;
45+
margin: 10px 0;
46+
display: grid;
47+
grid-template-columns: repeat(2, 1fr);
48+
gap: 8px;
49+
`;
50+
51+
codes.forEach((code, index) => {
52+
const codeEl = document.createElement('div');
53+
codeEl.className = 'recovery-code';
54+
codeEl.setAttribute('data-code-index', String(index));
55+
codeEl.innerText = code;
56+
codeEl.style.cssText = `
57+
padding: 5px 10px;
58+
background-color: white;
59+
border: 1px solid #ccc;
60+
border-radius: 3px;
61+
text-align: center;
62+
`;
63+
codesContainer.appendChild(codeEl);
64+
});
65+
66+
container.appendChild(codesContainer);
67+
68+
if (deviceName) {
69+
const deviceInfo = document.createElement('p');
70+
deviceInfo.id = 'recovery-codes-device';
71+
deviceInfo.innerText = `Device: ${deviceName}`;
72+
deviceInfo.style.color = '#666';
73+
container.appendChild(deviceInfo);
74+
}
75+
76+
const confirmationCallbacks =
77+
step.getCallbacksOfType<ConfirmationCallback>('ConfirmationCallback');
78+
79+
if (confirmationCallbacks.length > 0) {
80+
const confirmCb = confirmationCallbacks[0];
81+
const options = confirmCb.getOptions();
82+
83+
const optionsContainer = document.createElement('div');
84+
optionsContainer.style.marginTop = '15px';
85+
86+
options.forEach((option, index) => {
87+
const label = document.createElement('label');
88+
label.style.display = 'block';
89+
label.style.marginBottom = '5px';
90+
91+
const radio = document.createElement('input');
92+
radio.type = 'radio';
93+
radio.name = 'recovery-confirmation';
94+
radio.value = String(index);
95+
radio.checked = index === confirmCb.getDefaultOption();
96+
radio.addEventListener('change', () => {
97+
confirmCb.setOptionIndex(index);
98+
});
99+
100+
if (index === confirmCb.getDefaultOption()) {
101+
confirmCb.setOptionIndex(index);
102+
}
103+
104+
label.appendChild(radio);
105+
label.appendChild(document.createTextNode(` ${option}`));
106+
optionsContainer.appendChild(label);
107+
});
108+
109+
container.appendChild(optionsContainer);
110+
}
111+
112+
journeyEl.appendChild(container);
113+
114+
return true;
115+
}

e2e/journey-app/main.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { journey } from '@forgerock/journey-client';
1111
import type { RequestMiddleware } from '@forgerock/journey-client/types';
1212

1313
import { renderCallbacks } from './callback-map.js';
14+
import { renderQRCodeStep } from './components/qr-code.js';
15+
import { renderRecoveryCodesStep } from './components/recovery-codes.js';
1416
import { serverConfigs } from './server-configs.js';
1517

1618
const qs = window.location.search;
@@ -115,9 +117,13 @@ if (searchParams.get('middleware') === 'true') {
115117
header.innerText = formName || '';
116118
journeyEl.appendChild(header);
117119

118-
const callbacks = step.callbacks;
120+
const stepRendered =
121+
renderQRCodeStep(journeyEl, step) || renderRecoveryCodesStep(journeyEl, step);
119122

120-
renderCallbacks(journeyEl, callbacks);
123+
if (!stepRendered) {
124+
const callbacks = step.callbacks;
125+
renderCallbacks(journeyEl, callbacks);
126+
}
121127

122128
const submitBtn = document.createElement('button');
123129
submitBtn.type = 'submit';

e2e/journey-suites/src/device-profile.test.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,38 @@ import { username, password } from './utils/demo-user.js';
1111

1212
test('Test device profile collection journey flow', async ({ page }) => {
1313
const { clickButton, navigate } = asyncEvents(page);
14-
1514
const messageArray: string[] = [];
15+
let deviceProfileRequestBody: Record<string, unknown> | null = null;
1616

1717
page.on('console', async (msg) => {
1818
messageArray.push(msg.text());
1919
return Promise.resolve(true);
2020
});
2121

22+
page.on('request', (request) => {
23+
if (request.url().includes('/authenticate') && request.method() === 'POST') {
24+
try {
25+
const postData = request.postData();
26+
if (!postData) return;
27+
28+
const body = JSON.parse(postData);
29+
const deviceCallback = body.callbacks?.find(
30+
(cb: { type: string }) => cb.type === 'DeviceProfileCallback',
31+
);
32+
if (!deviceCallback) return;
33+
34+
const profileInput = deviceCallback.input?.find(
35+
(input: { name: string }) => input.name === 'IDToken1',
36+
);
37+
if (profileInput?.value) {
38+
deviceProfileRequestBody = JSON.parse(profileInput.value);
39+
}
40+
} catch {
41+
// Ignore parsing errors
42+
}
43+
}
44+
});
45+
2246
await navigate('/?journey=DeviceProfileCallbackTest&clientId=basic');
2347

2448
await expect(page.getByLabel('User Name')).toBeVisible({ timeout: 10000 });
@@ -35,7 +59,22 @@ test('Test device profile collection journey flow', async ({ page }) => {
3559

3660
await expect(page.getByText('Complete')).toBeVisible({ timeout: 15000 });
3761

38-
await clickButton('Logout', '/authenticate');
62+
expect(deviceProfileRequestBody).not.toBeNull();
63+
expect(deviceProfileRequestBody).toHaveProperty('identifier');
64+
expect(typeof deviceProfileRequestBody?.identifier).toBe('string');
65+
expect((deviceProfileRequestBody?.identifier as string).length).toBeGreaterThan(0);
66+
67+
expect(deviceProfileRequestBody).toHaveProperty('metadata');
68+
const metadata = deviceProfileRequestBody?.metadata as Record<string, unknown>;
69+
expect(metadata).toHaveProperty('hardware');
70+
expect(metadata).toHaveProperty('browser');
71+
expect(metadata).toHaveProperty('platform');
72+
73+
const platform = metadata.platform as Record<string, unknown>;
74+
expect(platform).toHaveProperty('deviceName');
75+
expect(typeof platform.deviceName).toBe('string');
76+
77+
await clickButton('Logout', '/sessions');
3978

4079
await expect(page.getByLabel('User Name')).toBeVisible({ timeout: 10000 });
4180

e2e/journey-suites/src/protect.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,38 @@ import { username, password } from './utils/demo-user.js';
1111

1212
test('Test PingOne Protect journey flow', async ({ page }) => {
1313
const { clickButton } = asyncEvents(page);
14-
1514
const messageArray: string[] = [];
15+
let protectSignalsData: string | null = null;
1616

1717
page.on('console', async (msg) => {
1818
messageArray.push(msg.text());
1919
return Promise.resolve(true);
2020
});
2121

22+
page.on('request', (request) => {
23+
if (request.url().includes('/authenticate') && request.method() === 'POST') {
24+
try {
25+
const postData = request.postData();
26+
if (postData) {
27+
const body = JSON.parse(postData);
28+
const callbacks = body.callbacks || [];
29+
for (const callback of callbacks) {
30+
if (callback.type === 'PingOneProtectEvaluationCallback') {
31+
const inputs = callback.input || [];
32+
for (const input of inputs) {
33+
if (input.name === 'IDToken1signals' && input.value) {
34+
protectSignalsData = input.value;
35+
}
36+
}
37+
}
38+
}
39+
}
40+
} catch {
41+
// Ignore parsing errors
42+
}
43+
}
44+
});
45+
2246
await page.goto('/?journey=TEST_LoginPingProtect&clientId=basic', { waitUntil: 'load' });
2347

2448
await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 });
@@ -36,12 +60,21 @@ test('Test PingOne Protect journey flow', async ({ page }) => {
3660
timeout: 15000,
3761
});
3862

63+
// Wait for the evaluation callback to auto-submit and complete
64+
await page.waitForResponse((response) => response.url().includes('/authenticate'));
65+
3966
await expect(page.getByText('Complete')).toBeVisible({ timeout: 15000 });
4067

41-
await clickButton('Logout', '/authenticate');
68+
// Verify signals were captured from the request
69+
expect(protectSignalsData).not.toBeNull();
70+
expect(typeof protectSignalsData).toBe('string');
71+
expect(protectSignalsData?.length).toBeGreaterThan(0);
72+
73+
await clickButton('Logout', '/sessions');
4274

4375
await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 });
4476

77+
// Verify the protect SDK flow through console logs
4578
expect(messageArray.some((msg) => msg.includes('Protect initialized successfully'))).toBe(true);
4679
expect(messageArray.some((msg) => msg.includes('Protect data collected successfully'))).toBe(
4780
true,

0 commit comments

Comments
 (0)