From 03b2325202eb32d28d62ecdf7a1bd754d5b7e761 Mon Sep 17 00:00:00 2001 From: Martin Jerberyd Date: Wed, 8 Oct 2025 15:40:33 +0200 Subject: [PATCH] Refactor QR refresh and status polling logic Refactored QR code refresh to use `setInterval` for periodic updates, replacing recursive timeouts. Introduced `startQrCodeRefresh` and `stopQrCodeRefresh` for better control and cleanup. Rewrote status polling with an asynchronous loop, adding a `statusPollingActive` flag to prevent duplicate loops. Improved flow control for cancellation and completion states. Refactored `postJson` to use iterative retries, enhancing error handling for non-JSON responses and unknown errors. Improved type safety by using `string | null` for nullable parameters and variables. Enhanced event listener handling with optional chaining for safer execution. Cleaned up redundant variables, consolidated logic, and added comments for better readability and maintainability. Updated UI visibility functions to handle `null` display values gracefully. --- .../Client/activelogin-main.ts | 278 +++++++++--------- 1 file changed, 145 insertions(+), 133 deletions(-) diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts b/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts index 2e4bfe4e..81a402a1 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts @@ -36,9 +36,8 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: // QR - var qrLastRefreshTimestamp: Date = null; var qrIsRefreshing = false; - var qrRefreshTimeoutId: number = null; + var qrRefreshIntervalId: number | null = null; // OrderRef @@ -63,6 +62,15 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: const uiResultForm = document.querySelector("form[name=activelogin-bankid-ui--result-form]"); const uiResultInput = uiResultForm.querySelector("input[name=uiResult]"); + // Flow control flags + var autoStartAttempts = 0; + var flowIsCancelledByUser = false; + var flowIsFinished = false; + + // Polling state + let statusPollingActive = false; + let currentOrderRef: string | null = null; + // Events if (sessionOrderRef) { @@ -108,19 +116,14 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: hide(qrCodeElement); } - // BankID - - var autoStartAttempts = 0; - var flowIsCancelledByUser = false; - var flowIsFinished = false; - - function enableCancelButton(requestVerificationToken: string, cancelUrl: string, protectedUiOptions: string, orderRef: string = null) { + function enableCancelButton(requestVerificationToken: string, cancelUrl: string, protectedUiOptions: string, orderRef: string | null = null) { var onCancelButtonClick = (event: Event) => { cancel(requestVerificationToken, cancelUrl, protectedUiOptions, orderRef); - event.target.removeEventListener("click", onCancelButtonClick); + event.target?.removeEventListener("click", onCancelButtonClick); }; cancelButtonElement.addEventListener("click", onCancelButtonClick); } + function initialize(requestVerificationToken: string, returnUrl: string, cancelUrl: string, protectedUiOptions: string) { flowIsCancelledByUser = false; @@ -140,7 +143,7 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: var startBankIdAppButtonOnClick = (event: Event) => { window.location.href = data.redirectUri; hide(startBankIdAppButtonElement); - event.target.removeEventListener("click", startBankIdAppButtonOnClick); + event.target?.removeEventListener("click", startBankIdAppButtonOnClick); }; startBankIdAppButtonElement.addEventListener("click", startBankIdAppButtonOnClick); @@ -152,8 +155,7 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: if (!!data.qrStartState && !!data.qrCodeAsBase64) { setQrCode(data.qrCodeAsBase64); - qrLastRefreshTimestamp = new Date(); - refreshQrCode(requestVerificationToken, data.qrStartState); + startQrCodeRefresh(requestVerificationToken, data.qrStartState); } enableCancelButton(requestVerificationToken, cancelUrl, protectedUiOptions, data.orderRef); @@ -172,92 +174,103 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: }); } + // Refactored: non-recursive status polling starter keeping original API name function checkStatus(requestVerificationToken: string, returnUrl: string, protectedUiOptions: string, orderRef: string) { - if (flowIsCancelledByUser || flowIsFinished) { - return; + currentOrderRef = orderRef; + if (statusPollingActive) { + return; // Avoid starting multiple loops } - - postJson(configuration.bankIdStatusApiUrl, - requestVerificationToken, - { - "orderRef": orderRef, - "returnUrl": returnUrl, - "uiOptions": protectedUiOptions, - "autoStartAttempts": autoStartAttempts - }, fetchRetryCountDefault) - .then(data => { - if (data.retryLogin) { - autoStartAttempts++; - login(); - } else if (data.isFinished) { - flowIsFinished = true; - clearTimeout(qrRefreshTimeoutId); + statusPollingActive = true; + (async () => { + while (!flowIsCancelledByUser && !flowIsFinished && currentOrderRef === orderRef) { + try { + const data = await postJson(configuration.bankIdStatusApiUrl, + requestVerificationToken, + { + "orderRef": orderRef, + "returnUrl": returnUrl, + "uiOptions": protectedUiOptions, + "autoStartAttempts": autoStartAttempts + }, + fetchRetryCountDefault); + + if (data.retryLogin) { + autoStartAttempts++; + statusPollingActive = false; // Stop current loop before re-login + login(); + return; + } else if (data.isFinished) { + flowIsFinished = true; + stopQrCodeRefresh(); + hide(qrCodeElement); + uiResultForm.setAttribute("action", data.redirectUri); + uiResultInput.value = data.result; + uiResultForm.submit(); + return; + } else if (!flowIsCancelledByUser) { + autoStartAttempts = 0; + showProgressStatus(data.statusMessage); + await delay(configuration.statusRefreshIntervalMs); + } + } catch (error: any) { + stopQrCodeRefresh(); + if (!flowIsCancelledByUser) { + showErrorStatus(error.message); + hide(startBankIdAppButtonElement); + } hide(qrCodeElement); - - uiResultForm.setAttribute("action", data.redirectUri); - uiResultInput.value = data.result; - uiResultForm.submit(); - } else if (!flowIsCancelledByUser) { - autoStartAttempts = 0; - showProgressStatus(data.statusMessage); - setTimeout(() => { - checkStatus(requestVerificationToken, returnUrl, protectedUiOptions, orderRef); - }, configuration.statusRefreshIntervalMs); - } - }) - .catch(error => { - clearTimeout(qrRefreshTimeoutId); - if (!flowIsCancelledByUser) { - showErrorStatus(error.message); - hide(startBankIdAppButtonElement); + statusPollingActive = false; + return; } - hide(qrCodeElement); - }); + } + statusPollingActive = false; + })(); } - function refreshQrCode(requestVerificationToken: string, qrStartState: string) { - if (flowIsCancelledByUser || flowIsFinished || qrIsRefreshing) { - return; - } + // Refactored: use setInterval instead of recursive timeouts + function startQrCodeRefresh(requestVerificationToken: string, qrStartState: string) { + stopQrCodeRefresh(); // Ensure single interval + qrRefreshIntervalId = window.setInterval(() => { + if (flowIsCancelledByUser || flowIsFinished) { + stopQrCodeRefresh(); + return; + } + if (qrIsRefreshing) { + return; + } + qrIsRefreshing = true; + postJson(configuration.bankIdQrCodeApiUrl, + requestVerificationToken, + { + "qrStartState": qrStartState + }, fetchRetryCountDefault) + .then(data => { + if (!!data.qrCodeAsBase64) { + setQrCode(data.qrCodeAsBase64); + } + }) + .catch(error => { + if (flowIsFinished) { + return; + } + if (!flowIsCancelledByUser) { + showErrorStatus(error.message); + hide(startBankIdAppButtonElement); + } + hide(qrCodeElement); + stopQrCodeRefresh(); + }) + .finally(() => { + qrIsRefreshing = false; + }); + }, configuration.qrCodeRefreshIntervalMs); + } - const currentTime = new Date(); - const timeSinceLastRefresh = currentTime.getTime() - qrLastRefreshTimestamp.getTime(); - if (timeSinceLastRefresh < configuration.qrCodeRefreshIntervalMs) { - qrRefreshTimeoutId = setTimeout(() => { - refreshQrCode(requestVerificationToken, qrStartState); - }, configuration.qrCodeRefreshIntervalMs); - return; + function stopQrCodeRefresh() { + if (qrRefreshIntervalId !== null) { + clearInterval(qrRefreshIntervalId); + qrRefreshIntervalId = null; } - qrIsRefreshing = true; - - postJson(configuration.bankIdQrCodeApiUrl, - requestVerificationToken, - { - "qrStartState": qrStartState - }, fetchRetryCountDefault) - .then(data => { - if (!!data.qrCodeAsBase64) { - qrLastRefreshTimestamp = new Date(); - setQrCode(data.qrCodeAsBase64); - qrRefreshTimeoutId = setTimeout(() => { - refreshQrCode(requestVerificationToken, qrStartState); - }, configuration.qrCodeRefreshIntervalMs); - } - }) - .catch(error => { - if (flowIsFinished) { - return; - } - - if (!flowIsCancelledByUser) { - showErrorStatus(error.message); - hide(startBankIdAppButtonElement); - } - hide(qrCodeElement); - }) - .finally(() => { - qrIsRefreshing = false; - }); } function setQrCode(qrCodeAsBase64: string) { @@ -265,8 +278,9 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: show(qrCodeElement); } - function cancel(requestVerificationToken: string, cancelReturnUrl: string, protectedUiOptions: string, orderRef: string = null) { + function cancel(requestVerificationToken: string, cancelReturnUrl: string, protectedUiOptions: string, orderRef: string | null = null) { flowIsCancelledByUser = true; + stopQrCodeRefresh(); if (!orderRef) { window.location.href = cancelReturnUrl; @@ -286,50 +300,48 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: // Helpers - function postJson(url: string, requestVerificationToken: string, data: any, retryCount: number = 0): Promise { - return fetch(url, - { - method: "POST", - headers: { - "Accept": "application/json", - "Content-Type": "application/json", - "RequestVerificationToken": requestVerificationToken - }, - credentials: 'include', - body: JSON.stringify(data) - }) - .catch(error => { - if (retryCount > 0) { - return delay(fetchRetryDelayMs).then(() => { - return postJson(url, requestVerificationToken, data, retryCount - 1); - }); + // Refactored: iterative retry instead of recursion + async function postJson(url: string, requestVerificationToken: string, data: any, retryCount: number = 0): Promise { + for (let attempt = 0; attempt <= retryCount; attempt++) { + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + "RequestVerificationToken": requestVerificationToken + }, + credentials: 'include', + body: JSON.stringify(data) + }); + + if (!response.ok) { + if (attempt < retryCount) { + await delay(fetchRetryDelayMs); + continue; + } + throw new Error(response.statusText || configuration.unknownErrorMessage); } - throw error; - }) - .then(response => { - if (!response.ok && retryCount > 0) { - return delay(fetchRetryDelayMs).then(() => { - return postJson(url, requestVerificationToken, data, retryCount - 1) - }); + const contentType = response.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + throw Error(configuration.unknownErrorMessage); } - return response; - }) - .then(response => { - const contentType = response.headers.get("content-type"); - if (contentType && contentType.indexOf("application/json") !== -1) { - return response.json(); + const json = await response.json(); + if (!!json.errorMessage) { + throw Error(json.errorMessage); } - - throw Error(configuration.unknownErrorMessage); - }) - .then(data => { - if (!!data.errorMessage) { - throw Error(data.errorMessage); + return json; + } catch (err) { + if (attempt < retryCount) { + await delay(fetchRetryDelayMs); + continue; } - return data; - }); + throw err; + } + } + throw new Error(configuration.unknownErrorMessage); // Fallback (should not reach) } function showProgressStatus(status: string) { @@ -347,7 +359,7 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: show(statusWrapperElement); } - function setVisibility(element: HTMLElement, visible: boolean, display: string = null) { + function setVisibility(element: HTMLElement, visible: boolean, display: string | null = null) { if (visible) { show(element, display); } else { @@ -355,8 +367,8 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: } } - function show(element: HTMLElement, display: string = "block") { - if (!element) { + function show(element: HTMLElement, display: string | null = "block") { + if (!element || display === null) { return; }