Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [11.6.2] - 2026-07-23

### Fixed

- DocumentCapture: Improve document capture UI in landscape when device lock is enabled or disabled

## [11.6.1] - 2026-07-17

### Fixed

- DocumentCapture: Keep the auto-capture UI consistent in landscape when device rotation is locked
- Embed: when a submission fails, show an actionable "check your connection" message for network drops (instead of the generic error) and update a single on-page message in place rather than stacking a new one on every retry. Applies across the Biometric KYC, eKYC, Basic KYC, Document Verification, Enhanced Document Verification, and SmartSelfie Authentication flows

## [11.6.0] - 2026-06-29

Expand Down
41 changes: 41 additions & 0 deletions packages/embed/cypress/tests/id-info.cy.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,45 @@ describe('id_info - Enhanced KYC', () => {
.should('contain', 'Nigeria')
.should('contain', 'National ID');
});

it('shows a single connection error when the upload fails on the skip path, no matter how many times "Yes, use this" is clicked', () => {
cy.visit('/biometric_kyc_id_info_complete');

cy.loadIDOptions();

// Selection and input screens are skipped, so accepting the selfie submits
// directly — the flow that previously stacked one error per click.
cy.getIFrameBody().find('smart-camera-web').should('be.visible');

// Simulate a network outage during upload.
cy.intercept(
{
method: 'POST',
url: '*upload*',
},
{ forceNetworkError: true },
).as('getUploadURL');

cy.navigateThroughCameraScreens();
cy.wait('@getUploadURL');

// A descriptive, actionable message is shown instead of the generic one.
cy.getIFrameBody()
.find('#submission-error-message')
.should('be.visible')
.should('contain', 'stable internet connection');

// Clicking "Yes, use this" again re-submits and fails again...
cy.getIFrameBody()
.find('smart-camera-web')
.shadow()
.find('selfie-capture-review')
.shadow()
.find('#select-id-image')
.click();
cy.wait('@getUploadURL');

// ...but the message is updated in place, never stacked.
cy.getIFrameBody().find('.validation-message').should('have.length', 1);
});
});
18 changes: 5 additions & 13 deletions packages/embed/src/js/basic-kyc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
captureInitApiFailure,
} from './init-api-sentry.js';
import { fetchWithTimeout } from './fetch-with-retry.js';
import {
displayErrorMessage,
submissionErrorMessage,
} from './submission-error.js';
import initIframeSentry from './sentry-iframe-init.js';

initIframeSentry('basic-kyc');
Expand Down Expand Up @@ -1026,7 +1030,7 @@ initIframeSentry('basic-kyc');
await submitIdInfoForm();
complete();
} catch (error) {
displayErrorMessage('Something went wrong');
displayErrorMessage(submissionErrorMessage(error, translate));
console.error(
`SmileIdentity - ${error.name || error.message}: ${error.cause}`,
);
Expand All @@ -1035,18 +1039,6 @@ initIframeSentry('basic-kyc');
}
}

function displayErrorMessage(message) {
const p = document.createElement('p');

p.textContent = message;
p.classList.add('validation-message');
p.style.fontSize = '1.5rem';
p.style.textAlign = 'center';

const main = document.querySelector('main');
main.prepend(p);
}

async function submitIdInfoForm() {
const { year, month, day, ...data } = id_info;
const dob = year && month && day ? `${year}-${month}-${day}` : undefined;
Expand Down
179 changes: 5 additions & 174 deletions packages/embed/src/js/biometric-kyc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import {
captureInitApiFailure,
} from './init-api-sentry.js';
import { fetchWithTimeout } from './fetch-with-retry.js';
import {
displayErrorMessage,
submissionErrorMessage,
} from './submission-error.js';
import initIframeSentry from './sentry-iframe-init.js';
import createOidcRedirect from './oidc/oidcRedirect';

Expand Down Expand Up @@ -1044,7 +1048,7 @@ window.Sentry = Sentry;

uploadZip(fileToUpload, uploadURL);
} catch (error) {
displayErrorMessage(translate('pages.error.generic'));
displayErrorMessage(submissionErrorMessage(error, translate));
console.error(
`SmileIdentity - ${error.name || error.message}: ${error.cause}`,
);
Expand All @@ -1053,179 +1057,6 @@ window.Sentry = Sentry;
}
}

function displayErrorMessage(message) {
const p = document.createElement('p');

p.textContent = message;
p.classList.add('validation-message');
p.style.fontSize = '1.5rem';
p.style.textAlign = 'center';

const main = document.querySelector('main');
main.prepend(p);
}

function oidcErrorCopy(result) {
if (!result) return translate('pages.oidc.genericError');
if (result.message === 'timeout') return translate('pages.oidc.timeout');
if (result.message === 'closed') return translate('pages.oidc.closed');
if (result.error === 'access_denied') {
return translate('pages.oidc.declined');
}
return translate('pages.oidc.genericError');
}

async function completeOidcFlow(state) {
// The popup exchange already persisted the claims server-side; the
// upload zip's info.json only carries `openid_state` so the biometric-kyc
// processor can read them and skip the partner-typed ID form entirely.
id_info = { ...id_info, openid_state: state, entered: false };
[uploadURL, fileToUpload] = await Promise.all([
getUploadURL(),
createZip(),
]);
uploadZip(fileToUpload, uploadURL);
}

function reportOidcError(result) {
[referenceWindow.parent, referenceWindow].forEach((win) => {
if (!win) return;
const outbound =
result && result.message === 'SmileIdentity::OidcCallback::Error'
? result
: {
message: 'SmileIdentity::OidcCallback::Error',
state: result && result.state,
error: result && result.message,
};
win.postMessage(outbound, '*');
});
}

function handleOidcFlow() {
if (activeScreen !== OidcRedirect) {
setActiveScreen(OidcRedirect);
}

const launchPanel = OidcRedirect.querySelector('#oidc-launch');
const loadingPanel = OidcRedirect.querySelector('#oidc-loading');
const preparingText = OidcRedirect.querySelector('#oidc-preparing');
const waitingText = OidcRedirect.querySelector('#oidc-waiting');
const popupBlockedPanel = OidcRedirect.querySelector('#oidc-popup-blocked');
const errorPanel = OidcRedirect.querySelector('#oidc-error');
const continueButton = OidcRedirect.querySelector('#oidc-continue-button');
const retryButton = OidcRedirect.querySelector('#oidc-retry-button');

const oidc = createOidcRedirect(config, {
country: id_info.country,
product: 'biometric_kyc',
});

// Pre-launch: show a spinner while the authorize URL is fetched. The
// Continue button only appears once we hold a link (see the prefetch
// chain below), so the user can't click before there's anywhere to send
// the popup.
function showPreparing() {
launchPanel.hidden = true;
popupBlockedPanel.hidden = true;
errorPanel.hidden = true;
errorPanel.textContent = '';
preparingText.hidden = false;
waitingText.hidden = true;
loadingPanel.hidden = false;
}

// The link is ready: reveal the explanation + Continue button. The popup
// is opened by the button click so `window.open` runs inside a user
// gesture — the one condition popup blockers honour.
function showLaunchButton() {
loadingPanel.hidden = true;
popupBlockedPanel.hidden = true;
errorPanel.hidden = true;
launchPanel.hidden = false;
}

// `retryable` keeps the launch panel (and its Continue button) visible so
// it doubles as a retry — clicking it re-runs `oidc.launch()`, which
// re-fetches the authorize URL. `report` gates the partner-facing
// OidcCallback::Error: a pre-click prefetch failure is recoverable and the
// user hasn't attempted anything yet, so we don't emit a terminal error
// event until an actual launch fails.
function showError(result, { retryable = false, report = true } = {}) {
launchPanel.hidden = !retryable;
loadingPanel.hidden = true;
popupBlockedPanel.hidden = true;
errorPanel.hidden = false;
errorPanel.textContent = oidcErrorCopy(result);
if (report) {
reportOidcError(result);
}
}

function showPopupBlocked() {
launchPanel.hidden = true;
loadingPanel.hidden = true;
popupBlockedPanel.hidden = false;
[referenceWindow.parent, referenceWindow].forEach((win) => {
if (win) {
win.postMessage(
{ message: 'SmileIdentity::OidcCallback::PopupBlocked' },
'*',
);
}
});
}

// Show the spinner up front and prefetch the authorize URL. Reveal the
// Continue button only once we hold a link. On failure, surface the error
// with the button as retry (it re-fetches), and hold the partner error
// event until the user actually attempts a launch.
showPreparing();
oidc
.prefetch()
.then(showLaunchButton)
.catch((error) => showError(error, { retryable: true, report: false }));

// Synchronous: opens the popup inside this gesture, then awaits the
// callback. Used for both the primary Continue button and the
// popup-blocked retry.
async function launch() {
launchPanel.hidden = true;
popupBlockedPanel.hidden = true;
errorPanel.hidden = true;
errorPanel.textContent = '';
preparingText.hidden = true;
waitingText.hidden = false;
loadingPanel.hidden = false;
try {
const { state } = await oidc.launch();
await completeOidcFlow(state);
} catch (result) {
if (result && result.message === 'popup_blocked') {
showPopupBlocked();
return;
}
showError(result);
}
}

continueButton.onclick = launch;
retryButton.onclick = launch;
}

OidcRedirect.querySelector('#oidc-back-button').addEventListener(
'click',
(event) => {
event.preventDefault();
const page = pages.pop();
if (page === SmartCameraWeb) {
page.reset();
}
setActiveScreen(page);
},
false,
);

async function createZip() {
const zip = new JSZip();

Expand Down
18 changes: 5 additions & 13 deletions packages/embed/src/js/doc-verification.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import {
idInfoToIdSelection,
} from './id-info-utils.js';
import { fetchWithTimeout } from './fetch-with-retry.js';
import {
displayErrorMessage,
submissionErrorMessage,
} from './submission-error.js';
import initIframeSentry from './sentry-iframe-init.js';
import { createDocSubmission } from './doc-submission.js';

Expand Down Expand Up @@ -804,25 +808,13 @@ window.Sentry = Sentry;
event.target.disabled = false;
} catch (error) {
event.target.disabled = false;
displayErrorMessage(t('pages.error.generic'));
displayErrorMessage(submissionErrorMessage(error, t));
console.error(
`SmileIdentity - ${error.name || error.message}: ${error.cause}`,
);
}
}

function displayErrorMessage(message) {
const p = document.createElement('p');

p.textContent = message;
p.classList.add('validation-message');
p.style.fontSize = '1.5rem';
p.style.textAlign = 'center';

const main = document.querySelector('main');
main.prepend(p);
}

async function createZip() {
const zip = new JSZip();

Expand Down
Loading
Loading