Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wip]Use expiresAt for QR payments #2520

Draft
wants to merge 5 commits into
base: v5
Choose a base branch
from
Draft
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
8 changes: 8 additions & 0 deletions .changeset/slimy-pillows-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@adyen/adyen-web": minor
---

For the following QR based payments - `bcmc_mobile`, `duitnow`, `payme`, `paynow`, `pix`, `promptpay`, `swish` and `wechatpayQR`, we improved how we calculate the countdown time.

Specifically, we calculate the QR countdown time based on the `expiresAt` timestamp from the `/payments` response if it is returned in the action object, otherwise we use merchant's frontend configuration.
If both are not presented, we fall back to the default value.
17 changes: 7 additions & 10 deletions packages/lib/src/components/BcmcMobile/BcmcMobile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@ import { STATUS_INTERVAL, COUNTDOWN_MINUTES } from './config';

class BCMCMobileElement extends QRLoaderContainer {
public static type = 'bcmc_mobile';
private static isMobile = window.matchMedia('(max-width: 768px)').matches && /Android|iPhone|iPod/.test(navigator.userAgent);

formatProps(props) {
const isMobile = window.matchMedia('(max-width: 768px)').matches && /Android|iPhone|iPod/.test(navigator.userAgent);

return {
delay: STATUS_INTERVAL,
countdownTime: COUNTDOWN_MINUTES,
buttonLabel: isMobile ? 'openApp' : 'generateQRCode',
...super.formatProps(props)
};
}
protected static defaultProps = {
delay: STATUS_INTERVAL,
countdownTime: COUNTDOWN_MINUTES,
buttonLabel: BCMCMobileElement.isMobile ? 'openApp' : 'generateQRCode',
...QRLoaderContainer.defaultProps
};
}

export default BCMCMobileElement;
12 changes: 5 additions & 7 deletions packages/lib/src/components/DuitNow/DuitNow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import { delay, countdownTime } from './config';
class DuitNowElement extends QRLoaderContainer {
public static type = 'duitnow';

formatProps(props) {
return {
delay,
countdownTime,
...super.formatProps(props)
};
}
protected static defaultProps = {
countdownTime,
delay,
...QRLoaderContainer.defaultProps
};
}

export default DuitNowElement;
22 changes: 10 additions & 12 deletions packages/lib/src/components/PayMe/PayMe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@ class PayMeElement extends QRLoaderContainer {
private static defaultCountdown = 10; // min
private static defaultDelay = 2000; // ms

formatProps(props) {
return {
delay: PayMeElement.defaultDelay,
countdownTime: PayMeElement.defaultCountdown,
redirectIntroduction: 'payme.openPayMeApp',
introduction: 'payme.scanQrCode',
timeToPay: 'payme.timeToPay',
buttonLabel: 'payme.redirectButtonLabel',
instructions: Instructions,
...super.formatProps(props)
};
}
protected static defaultProps = {
delay: PayMeElement.defaultDelay,
countdownTime: PayMeElement.defaultCountdown,
redirectIntroduction: 'payme.openPayMeApp',
introduction: 'payme.scanQrCode',
timeToPay: 'payme.timeToPay',
buttonLabel: 'payme.redirectButtonLabel',
instructions: Instructions,
...QRLoaderContainer.defaultProps
};
}

export default PayMeElement;
12 changes: 5 additions & 7 deletions packages/lib/src/components/PayNow/PayNow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import { delay, countdownTime } from './config';
class PayNowElement extends QRLoaderContainer {
public static type = 'paynow';

formatProps(props) {
return {
delay,
countdownTime,
...super.formatProps(props)
};
}
protected static defaultProps = {
countdownTime,
delay,
...QRLoaderContainer.defaultProps
};
}

export default PayNowElement;
12 changes: 5 additions & 7 deletions packages/lib/src/components/PromptPay/PromptPay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import { delay, countdownTime } from './config';
class PromptPayElement extends QRLoaderContainer {
public static type = 'promptpay';

formatProps(props) {
return {
delay,
countdownTime,
...super.formatProps(props)
};
}
protected static defaultProps = {
countdownTime,
delay,
...QRLoaderContainer.defaultProps
};
}

export default PromptPayElement;
15 changes: 7 additions & 8 deletions packages/lib/src/components/Swish/Swish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import QRLoaderContainer from '../helpers/QRLoaderContainer';

class SwishElement extends QRLoaderContainer {
public static type = 'swish';
formatProps(props) {
return {
delay: 2000, // ms
countdownTime: 3, // min
instructions: 'swish.pendingMessage',
...super.formatProps(props)
};
}

protected static defaultProps = {
delay: 2000, // ms
countdownTime: 3, // min
instructions: 'swish.pendingMessage',
...QRLoaderContainer.defaultProps
};
}

export default SwishElement;
20 changes: 19 additions & 1 deletion packages/lib/src/components/WeChat/WeChat.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import WeChat from './WeChat';
import * as utils from '../../utils/getTimeDiffInMinutesFromNow';
import { countdownTime } from './config';

const calculateTimeDiffMock = jest.spyOn(utils, 'getTimeDiffInMinutesFromNow').mockImplementation(() => 5);

describe('WeChat', () => {
describe('formatProps', () => {});
describe('formatProps', () => {
test('should calculate the time difference if expiresAt exists', () => {
const expiresAt = '2024-01-15T14:00:48.321283089Z';
const wechat = new WeChat({ expiresAt });
expect(calculateTimeDiffMock).toHaveBeenCalledWith(expiresAt, wechat.props.delay);
});
test('should use the countdownTime from the props if it exists', () => {
const wechat = new WeChat({ countdownTime: 3 });
expect(wechat.props.countdownTime).toBe(3);
});
test('should use the default countdownTime if neither expiresAt nor countdownTime value exists', () => {
const wechat = new WeChat({});
expect(wechat.props.countdownTime).toBe(countdownTime);
});
});

describe('isValid', () => {
test('should be always true', () => {
Expand Down
12 changes: 5 additions & 7 deletions packages/lib/src/components/WeChat/WeChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import { delay, countdownTime } from './config';
class WeChatPayElement extends QRLoaderContainer {
public static type = 'wechatpayQR';

formatProps(props) {
return {
delay,
countdownTime,
...super.formatProps(props)
};
}
protected static defaultProps = {
countdownTime,
delay,
...QRLoaderContainer.defaultProps
};
}

export default WeChatPayElement;
28 changes: 26 additions & 2 deletions packages/lib/src/components/helpers/QRLoaderContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import QRLoader from '../internal/QRLoader';
import CoreProvider from '../../core/Context/CoreProvider';
import RedirectButton from '../internal/RedirectButton';
import SRPanelProvider from '../../core/Errors/SRPanelProvider';
import { getTimeDiffInMinutesFromNow } from '../../utils/getTimeDiffInMinutesFromNow';

export interface QRLoaderContainerProps extends UIElementProps {
/**
* Number of miliseconds that the component will wait in between status calls
* Number of milliseconds that the component will wait in between status calls
*/
delay?: number;

Expand All @@ -17,12 +18,19 @@ export interface QRLoaderContainerProps extends UIElementProps {
*/
countdownTime?: number;

/**
* UTC timestamp from the /payments response.
* If it presents we should calculate the countdown time between the expiresAt and the current local time.
* Otherwise, it falls back to the merchant's countdownTime prop or the default one per QR payment.
*/
expiresAt?: string;

type?: string;
brandLogo?: string;
buttonLabel?: string;
qrCodeImage?: string;
paymentData?: string;
introduction: string;
introduction?: string;
redirectIntroduction?: string;
timeToPay?: string;
instructions?: string | (() => h.JSX.Element);
Expand All @@ -40,6 +48,22 @@ class QRLoaderContainer<T extends QRLoaderContainerProps = QRLoaderContainerProp
onActionHandled: () => {}
};

formatProps(props) {
return {
...props,
countdownTime: this.getCountDownTime(props)
};
}

getCountDownTime({ expiresAt, delay, countdownTime }): number {
try {
return expiresAt ? getTimeDiffInMinutesFromNow(expiresAt, delay) : countdownTime;
} catch (e) {
console.warn(e?.message);
return countdownTime;
}
}

formatData() {
return {
paymentMethod: {
Expand Down
20 changes: 20 additions & 0 deletions packages/lib/src/utils/getTimeDiffInMinutesFromNow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getTimeDiffInMinutesFromNow } from './getTimeDiffInMinutesFromNow';

describe('getTimeDiffInMinutesFromNow', () => {
test('should return the time difference in minutes without delay', () => {
const fiveMinutes = 5;
const futureTime = new Date(Date.now() + 1000 * 60 * fiveMinutes);
expect(getTimeDiffInMinutesFromNow(futureTime.toISOString())).toEqual(fiveMinutes);
});

test('should return the time difference in minutes with a delay', () => {
const fiveMinutes = 5;
const delay = 1000 * 60 * fiveMinutes;
const futureTime = new Date(Date.now() + delay);
expect(getTimeDiffInMinutesFromNow(futureTime.toISOString(), delay)).toEqual(0);
});

test('should throw an error when the time duration cannot be calculated', () => {
expect(() => getTimeDiffInMinutesFromNow('wrong datetime')).toThrow();
});
});
15 changes: 15 additions & 0 deletions packages/lib/src/utils/getTimeDiffInMinutesFromNow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Calculate the time difference in minutes, between the future UTC time and the current time (plus delay).
* @param futureTime - UTC date time string in the ISO 8601 format
* @param delayFromNow - milliseconds delay from now
*/
export function getTimeDiffInMinutesFromNow(futureTime: string, delayFromNow = 0) {
const future = new Date(futureTime);
const now = new Date();
now.setTime(now.getTime() + delayFromNow);
const diff = (future.getTime() - now.getTime()) / 60000;
if (Number.isNaN(diff) || diff < 0) {
throw new Error('Invalid countdown duration. A default one will be used.');
}
return diff;
}
Loading