mirror of
https://github.com/jlengrand/adyen-web.git
synced 2026-03-10 08:01:22 +00:00
Refactor/improve QR payments status check (#2506)
* refactor: improve the QR payments status check call, wait for the previous call to finish * added changeset * refactor: added the timeout for the default http calls and the status check call * refactor: add unit test
This commit is contained in:
5
.changeset/gorgeous-cameras-grab.md
Normal file
5
.changeset/gorgeous-cameras-grab.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@adyen/adyen-web': patch
|
||||
---
|
||||
|
||||
Improve the payment status check call for QR payments.
|
||||
@@ -76,14 +76,14 @@ function Await(props: AwaitComponentProps) {
|
||||
};
|
||||
|
||||
const checkStatus = (): void => {
|
||||
const { paymentData, clientKey } = props;
|
||||
const { paymentData, clientKey, throttleInterval } = props;
|
||||
|
||||
if (!hasCalledActionHandled) {
|
||||
props.onActionHandled({ componentType: props.type, actionDescription: 'polling-started' });
|
||||
setHasCalledActionHandled(true);
|
||||
}
|
||||
|
||||
checkPaymentStatus(paymentData, clientKey, loadingContext)
|
||||
checkPaymentStatus(paymentData, clientKey, loadingContext, throttleInterval)
|
||||
.then(processResponse)
|
||||
.catch(({ message, ...response }) => ({
|
||||
type: 'network-error',
|
||||
|
||||
@@ -5,6 +5,7 @@ import checkPaymentStatus from '../../../core/Services/payment-status';
|
||||
import Language from '../../../language/Language';
|
||||
|
||||
jest.mock('../../../core/Services/payment-status');
|
||||
jest.useFakeTimers();
|
||||
|
||||
const i18n = { get: key => key } as Language;
|
||||
|
||||
@@ -15,6 +16,11 @@ describe('WeChat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllTimers();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('checkStatus', () => {
|
||||
// Pending status
|
||||
test('checkStatus processes a pending response', () => {
|
||||
@@ -84,5 +90,32 @@ describe('WeChat', () => {
|
||||
expect(onErrorMock.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('statusInterval', () => {
|
||||
let qrLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
const checkPaymentStatusValue = { payload: 'Ab02b4c0!', resultCode: 'pending', type: 'complete' };
|
||||
(checkPaymentStatus as jest.Mock).mockResolvedValue(checkPaymentStatusValue);
|
||||
});
|
||||
|
||||
test('should set a timeout recursively', async () => {
|
||||
jest.spyOn(global, 'setTimeout');
|
||||
qrLoader = new QRLoader({ delay: 1000 });
|
||||
qrLoader.statusInterval();
|
||||
expect(setTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
|
||||
await jest.runOnlyPendingTimersAsync();
|
||||
expect(setTimeout).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('should change the delay to the throttledInterval if the timePassed exceeds the throttleTime', async () => {
|
||||
jest.spyOn(global, 'setTimeout');
|
||||
qrLoader = new QRLoader({ throttleTime: 0, throttledInterval: 2000, delay: 1000 });
|
||||
qrLoader.statusInterval();
|
||||
expect(setTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import useAutoFocus from '../../../utils/useAutoFocus';
|
||||
const QRCODE_URL = 'barcode.shtml?barcodeType=qrCode&fileType=png&data=';
|
||||
|
||||
class QRLoader extends Component<QRLoaderProps, QRLoaderState> {
|
||||
private interval;
|
||||
private timeoutId;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -46,35 +46,39 @@ class QRLoader extends Component<QRLoaderProps, QRLoaderState> {
|
||||
introduction: 'wechatpay.scanqrcode'
|
||||
};
|
||||
|
||||
// Retry until getting a complete response from the server or it times out\
|
||||
// Changes interval time to 10 seconds after 1 minute (60 seconds)
|
||||
public statusInterval = () => {
|
||||
this.checkStatus();
|
||||
|
||||
this.setState({ timePassed: this.state.timePassed + this.props.delay });
|
||||
|
||||
if (this.state.timePassed >= this.props.throttleTime) {
|
||||
this.setState({ delay: this.props.throttledInterval });
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.interval = setInterval(this.statusInterval, this.state.delay);
|
||||
}
|
||||
|
||||
public redirectToApp = url => {
|
||||
window.location.assign(url);
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.delay !== this.state.delay) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = setInterval(this.statusInterval, this.state.delay);
|
||||
}
|
||||
this.statusInterval();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
|
||||
public redirectToApp = (url: string | URL) => {
|
||||
window.location.assign(url);
|
||||
};
|
||||
|
||||
// Retry until getting a complete response from the server, or it times out
|
||||
public statusInterval = (responseTime = 0) => {
|
||||
// If we are already in the final statuses, do not poll!
|
||||
if (this.state.expired || this.state.completed) return;
|
||||
|
||||
this.setState(previous => ({ timePassed: previous.timePassed + this.props.delay + responseTime }));
|
||||
// Changes interval time to 10 seconds after 1 minute (60 seconds)
|
||||
const newDelay = this.state.timePassed >= this.props.throttleTime ? this.props.throttledInterval : this.state.delay;
|
||||
this.pollStatus(newDelay);
|
||||
};
|
||||
|
||||
private pollStatus(delay: number) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = setTimeout(async () => {
|
||||
// Wait for previous status call to finish.
|
||||
// Also taking the server response time into the consideration to calculate timePassed.
|
||||
const start = performance.now();
|
||||
await this.checkStatus();
|
||||
const end = performance.now();
|
||||
this.statusInterval(Math.round(end - start));
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private onTick = (time): void => {
|
||||
@@ -83,12 +87,12 @@ class QRLoader extends Component<QRLoaderProps, QRLoaderState> {
|
||||
|
||||
private onTimeUp = (): void => {
|
||||
this.setState({ expired: true });
|
||||
clearInterval(this.interval);
|
||||
clearTimeout(this.timeoutId);
|
||||
this.props.onError(new AdyenCheckoutError('ERROR', 'Payment Expired'));
|
||||
};
|
||||
|
||||
private onComplete = (status: StatusObject): void => {
|
||||
clearInterval(this.interval);
|
||||
clearTimeout(this.timeoutId);
|
||||
this.setState({ completed: true, loading: false });
|
||||
|
||||
const state = {
|
||||
@@ -102,7 +106,7 @@ class QRLoader extends Component<QRLoaderProps, QRLoaderState> {
|
||||
};
|
||||
|
||||
private onError = (status: StatusObject): void => {
|
||||
clearInterval(this.interval);
|
||||
clearTimeout(this.timeoutId);
|
||||
this.setState({ expired: true, loading: false });
|
||||
|
||||
if (status.props.payload) {
|
||||
@@ -120,9 +124,9 @@ class QRLoader extends Component<QRLoaderProps, QRLoaderState> {
|
||||
};
|
||||
|
||||
private checkStatus = () => {
|
||||
const { paymentData, clientKey, loadingContext } = this.props;
|
||||
const { paymentData, clientKey, loadingContext, throttledInterval } = this.props;
|
||||
|
||||
return checkPaymentStatus(paymentData, clientKey, loadingContext)
|
||||
return checkPaymentStatus(paymentData, clientKey, loadingContext, throttledInterval)
|
||||
.then(processResponse)
|
||||
.catch(response => ({ type: 'network-error', props: response }))
|
||||
.then((status: StatusObject) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fetch from './fetch';
|
||||
import { FALLBACK_CONTEXT } from '../config';
|
||||
import { DEFAULT_HTTP_TIMEOUT, FALLBACK_CONTEXT } from '../config';
|
||||
import AdyenCheckoutError from '../Errors/AdyenCheckoutError';
|
||||
|
||||
interface HttpOptions {
|
||||
@@ -11,6 +11,7 @@ interface HttpOptions {
|
||||
method?: string;
|
||||
path: string;
|
||||
errorLevel?: ErrorLevel;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
type ErrorLevel = 'silent' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
@@ -27,7 +28,7 @@ function isAdyenErrorResponse(data: any): data is AdyenErrorResponse {
|
||||
}
|
||||
|
||||
export function http<T>(options: HttpOptions, data?: any): Promise<T> {
|
||||
const { headers = [], errorLevel = 'warn', loadingContext = FALLBACK_CONTEXT, method = 'GET', path } = options;
|
||||
const { headers = [], errorLevel = 'warn', loadingContext = FALLBACK_CONTEXT, method = 'GET', path, timeout = DEFAULT_HTTP_TIMEOUT } = options;
|
||||
|
||||
const request: RequestInit = {
|
||||
method,
|
||||
@@ -41,6 +42,7 @@ export function http<T>(options: HttpOptions, data?: any): Promise<T> {
|
||||
},
|
||||
redirect: 'follow',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
...(AbortSignal?.timeout && { signal: AbortSignal?.timeout(timeout) }),
|
||||
...(data && { body: JSON.stringify(data) })
|
||||
};
|
||||
|
||||
|
||||
@@ -5,16 +5,18 @@ import { httpPost } from './http';
|
||||
* @param paymentData -
|
||||
* @param clientKey -
|
||||
* @param loadingContext -
|
||||
* @param timeout - in milliseconds
|
||||
* @returns a promise containing the response of the call
|
||||
*/
|
||||
export default function checkPaymentStatus(paymentData, clientKey, loadingContext) {
|
||||
export default function checkPaymentStatus(paymentData, clientKey, loadingContext, timeout) {
|
||||
if (!paymentData || !clientKey) {
|
||||
throw new Error('Could not check the payment status');
|
||||
}
|
||||
|
||||
const options = {
|
||||
loadingContext,
|
||||
path: `services/PaymentInitiation/v1/status?clientKey=${clientKey}`
|
||||
path: `services/PaymentInitiation/v1/status?clientKey=${clientKey}`,
|
||||
timeout
|
||||
};
|
||||
|
||||
return httpPost(options, { paymentData });
|
||||
|
||||
@@ -34,7 +34,10 @@ export const GENERIC_OPTIONS = [
|
||||
'setStatusAutomatically'
|
||||
];
|
||||
|
||||
export const DEFAULT_HTTP_TIMEOUT = 60000;
|
||||
|
||||
export default {
|
||||
FALLBACK_CONTEXT,
|
||||
GENERIC_OPTIONS
|
||||
GENERIC_OPTIONS,
|
||||
DEFAULT_HTTP_TIMEOUT
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user