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

PayNow improvements #2889

Merged
merged 12 commits into from
Oct 18, 2024
5 changes: 5 additions & 0 deletions .changeset/tricky-pets-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adyen/adyen-web': minor
---

PayNow - Adding instructions to scan QR code on mobile view
56 changes: 56 additions & 0 deletions packages/lib/src/components/PayNow/PayNow.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import PayNow from './PayNow';
import { render, screen, within } from '@testing-library/preact';
import { mock } from 'jest-mock-extended';
import { Resources } from '../../core/Context/Resources';
import checkPaymentStatus from '../../core/Services/payment-status';
import { SRPanel } from '../../core/Errors/SRPanel';

jest.mock('../../core/Services/payment-status');

describe('PayNow', () => {
describe('isValid', () => {
Expand All @@ -21,4 +28,53 @@ describe('PayNow', () => {
expect(paynow.render()).not.toBe(null);
});
});

test('should render mobile instructions', async () => {
// Mocks matchMedia to return 'matches: true' when checking (max-width: 1024px)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(() => ({
matches: true
}))
});

jest.useFakeTimers();

const srPanel = mock<SRPanel>();
const resources = mock<Resources>();
resources.getImage.mockReturnValue((icon: string) => `https://checkout-adyen.com/${icon}`);

// @ts-ignore mockResolvedValue not inferred
checkPaymentStatus.mockResolvedValue({
payload: 'Ab02b4c0!BQABAgBLLk9evMb+rScNdE...',
resultCode: 'pending',
type: 'complete'
});

const paynow = new PayNow(global.core, {
loadingContext: 'checkoutshopper.com/',
modules: { resources, analytics: global.analytics, srPanel },
i18n: global.i18n,
paymentData: 'Ab02b4c0!BQABAgBH1f8hqfFxOvbfK..',
qrCodeImage: '',
paymentMethodType: 'paynow',
qrCodeData: '00020126580009SG...'
});

render(paynow.mount('body'));
ribeiroguilherme marked this conversation as resolved.
Show resolved Hide resolved

// Triggers the execution of the setTimeout that makes the /status API request
jest.runAllTimers();

await screen.findAllByText(/Scan the QR code using the PayNow app to complete the payment/);

const div = within(screen.queryByTestId('paynow-introduction'));
div.getByText(/Take a screenshot of the QR code./);
div.getByText(/Open the PayNow bank or payment app./);
div.getByText(/Select the option to scan a QR code./);
div.getByText(/Choose the option to upload a QR and select the screenshot./);
div.getByText(/Complete the transaction./);

jest.resetAllMocks();
});
});
5 changes: 5 additions & 0 deletions packages/lib/src/components/PayNow/PayNow.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import QRLoaderContainer from '../helpers/QRLoaderContainer/QRLoaderContainer';
import { delay, countdownTime } from './config';
import { TxVariants } from '../tx-variants';
import { PayNowIntroduction } from './components/PayNowIntroduction';
import { PayNowInstructions } from './components/PayNowInstructions';

class PayNowElement extends QRLoaderContainer {
public static type = TxVariants.paynow;

formatProps(props) {
return {
introduction: PayNowIntroduction,
instructions: PayNowInstructions,
timeToPay: 'payme.timeToPay',
delay,
countdownTime,
...super.formatProps(props)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@import 'styles/variable-generator';

.adyen-checkout-paynow__instructions {
font-size: token(text-body-font-size);
font-weight: token(text-body-font-weight);
line-height: token(text-body-line-height);
color: token(color-label-primary);
text-align: center;
}

.adyen-checkout-paynow__instructions > p {
margin-bottom: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { h } from 'preact';
import { useCoreContext } from '../../../core/Context/CoreProvider';
import ContentSeparator from '../../internal/ContentSeparator';
import './PayNowInstructions.scss';
import { useIsMobile } from '../../../utils/useIsMobile';

const PayNowInstructions = () => {
const { i18n } = useCoreContext();
const { isMobileScreenSize } = useIsMobile();

if (!isMobileScreenSize) return;

return (
<div className="adyen-checkout-paynow__instructions">
<ContentSeparator />
<p>{i18n.get('paynow.scanQrCode')}</p>
</div>
);
};

export { PayNowInstructions };
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import 'styles/variable-generator';

.adyen-checkout-paynow__introduction {
font-size: token(text-body-font-size);
font-weight: token(text-body-font-weight);
line-height: token(text-body-line-height);
color: token(color-label-primary);
text-align: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { h } from 'preact';
import { useCoreContext } from '../../../core/Context/CoreProvider';
import './PayNowIntroduction.scss';
import { TimelineWrapper, Timeline } from '../../internal/Timeline';
import { useIsMobile } from '../../../utils/useIsMobile';

const PayNowIntroduction = () => {
const { i18n } = useCoreContext();
const { isMobileScreenSize } = useIsMobile();

const instructions = [
i18n.get('paynow.mobileViewInstruction.step1'),
i18n.get('paynow.mobileViewInstruction.step2'),
i18n.get('paynow.mobileViewInstruction.step3'),
i18n.get('paynow.mobileViewInstruction.step4'),
i18n.get('paynow.mobileViewInstruction.step5')
];

return (
<div className="adyen-checkout-paynow__introduction" data-testid="paynow-introduction">
{isMobileScreenSize ? (
<TimelineWrapper>
<Timeline instructions={instructions} />
</TimelineWrapper>
) : (
i18n.get('paynow.scanQrCode')
)}
</div>
);
};

export { PayNowIntroduction };
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ export interface QRLoaderConfiguration extends UIElementProps {
brandLogo?: string;
buttonLabel?: string;
qrCodeImage?: string;
qrCodeData?: string;
paymentData?: string;
introduction?: string;
redirectIntroduction?: string;
timeToPay?: string;
instructions?: string | (() => h.JSX.Element);
copyBtn?: boolean;
introduction?: string | (() => h.JSX.Element);
instructions?: string | (() => h.JSX.Element);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
display: flex;
justify-content: center;
align-items: center;
color: token(color-label-secondary);
color: token(color-label-primary);
white-space: nowrap;
font-size: 13px;
text-transform: capitalize;
font-size: token(text-body-font-size);
line-height: token(text-caption-line-height);

&::after,
&::before {
content: '';
flex: 1;
border-bottom: 1px solid token(color-outline-tertiary);
border-bottom: 1px solid token(color-separator-primary);
}

&::after {
Expand Down
43 changes: 25 additions & 18 deletions packages/lib/src/components/internal/QRLoader/QRLoader.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
.adyen-checkout__qr-loader {
background: token(color-background-primary);
padding: token(spacer-110);
padding-bottom: token(spacer-090);
border: token(border-width-s) solid token(color-outline-secondary);
border-radius: token(border-radius-m);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}

.adyen-checkout__qr-loader--result {
padding: 100px;
gap: token(spacer-090);
}

.adyen-checkout__qr-loader--app {
Expand All @@ -18,22 +22,20 @@
padding: 0;
}

.adyen-checkout__qr-loader__brand-logo-wrapper {
border-radius: token(border-radius-s);
box-shadow: token(shadow-low);
margin-bottom: token(spacer-090);
overflow: hidden;
}

.adyen-checkout__qr-loader__brand-logo {
width: 74px;
border-radius: 3px;
width: 80px;
display: block;
}

.adyen-checkout__qr-loader__subtitle {
max-width: 400px;
margin: token(spacer-100) auto 0;
}

.adyen-checkout__qr-loader__subtitle--result {
margin-bottom: token(spacer-100);
}

.adyen-checkout__qr-loader__subtitle,
.adyen-checkout__qr-loader__payment_amount {
color: token(color-label-primary);
font-size: token(text-subtitle-font-size);
line-height: token(text-caption-line-height);
Expand All @@ -45,16 +47,20 @@
}

.adyen-checkout__qr-loader__payment_amount {
font-weight: bold;
color: token(color-label-primary);
font-weight: token(text-title-l-font-weight);
font-size: token(text-title-l-font-size);
line-height: token(text-body-wide-line-height);
margin-bottom: token(spacer-090);
}

.adyen-checkout__qr-loader__progress {
height: token(spacer-020);
background: token(color-outline-secondary);
border-radius: token(border-radius-l);
margin: token(spacer-100) auto token(spacer-060);
width: 152px;
padding-right: 3%;
margin-bottom: token(spacer-060);

[dir="rtl"] & {
padding-right: 0;
Expand All @@ -70,8 +76,8 @@
}

.adyen-checkout__qr-loader__countdown {
color: token(color-label-secondary);
font-size: token(text-body-font-size);
font-size: token(text-caption-font-size);
text-align: center;
}

.adyen-checkout__qr-loader > .adyen-checkout__spinner__wrapper {
Expand All @@ -88,10 +94,11 @@
}

.adyen-checkout__qr-loader__instructions {
max-width: 300px;
color: token(color-label-tertiary);
font-size: token(text-subtitle-font-size);
line-height: 1.5;
margin-top: token(spacer-100);
margin-top: token(spacer-040);
}

.adyen-checkout__qr-loader__actions {
Expand All @@ -101,7 +108,7 @@
margin-top: token(spacer-100);
}

@media only screen and (max-device-width: 1200px) {
@media only screen and (max-width: 1200px) {
.adyen-checkout__qr-loader__app-link {
display: block;
}
Expand Down
24 changes: 16 additions & 8 deletions packages/lib/src/components/internal/QRLoader/QRLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ class QRLoader extends Component<QRLoaderProps, QRLoaderState> {
src={getImage({ imageFolder: 'components/' })(image)}
alt={status}
/>
<div className="adyen-checkout__qr-loader__subtitle adyen-checkout__qr-loader__subtitle--result">{status}</div>
<div className="adyen-checkout__qr-loader__subtitle">{status}</div>
</div>
);
};
Expand All @@ -180,7 +180,11 @@ class QRLoader extends Component<QRLoaderProps, QRLoaderState> {
if (loading) {
return (
<div className="adyen-checkout__qr-loader">
{brandLogo && <img alt={brandName} src={brandLogo} className="adyen-checkout__qr-loader__brand-logo" />}
{brandLogo && (
<div className="adyen-checkout__qr-loader__brand-logo-wrapper">
<img alt={brandName} src={brandLogo} className="adyen-checkout__qr-loader__brand-logo" />
</div>
)}
<Spinner />
</div>
);
Expand All @@ -193,7 +197,11 @@ class QRLoader extends Component<QRLoaderProps, QRLoaderState> {

return (
<div className={`adyen-checkout__qr-loader adyen-checkout__qr-loader--${type} ${classnames.join(' ')}`}>
{brandLogo && <img src={brandLogo} alt={brandName} className="adyen-checkout__qr-loader__brand-logo" />}
{brandLogo && (
<div className="adyen-checkout__qr-loader__brand-logo-wrapper">
<img src={brandLogo} alt={brandName} className="adyen-checkout__qr-loader__brand-logo" />
</div>
)}

{amount && amount.value && amount.currency && (
<div className="adyen-checkout__qr-loader__payment_amount">{i18n.amount(amount.value, amount.currency)}</div>
Expand All @@ -211,7 +219,7 @@ class QRLoader extends Component<QRLoaderProps, QRLoaderState> {

{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
<div ref={qrSubtitleRef} tabIndex={0} className="adyen-checkout__qr-loader__subtitle">
{i18n.get(this.props.introduction)}
{typeof this.props.introduction === 'string' ? i18n.get(this.props.introduction) : this.props.introduction?.()}
</div>

<img
Expand All @@ -232,10 +240,10 @@ class QRLoader extends Component<QRLoaderProps, QRLoaderState> {
&nbsp;{timeToPayString[1]}
</div>

{typeof this.props.instructions === 'string' ? (
<div className="adyen-checkout__qr-loader__instructions">{i18n.get(this.props.instructions)}</div>
) : (
this.props.instructions?.()
{this.props.instructions && (
<div className="adyen-checkout__qr-loader__instructions">
{typeof this.props.instructions === 'string' ? i18n.get(this.props.instructions) : this.props.instructions?.()}
</div>
)}

{this.props.copyBtn && (
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/src/components/internal/QRLoader/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export interface QRLoaderProps {
brandLogo?: string;
brandName?: string;
buttonLabel?: string;
introduction?: string;
redirectIntroduction?: string;
timeToPay?: string;
introduction?: string | (() => h.JSX.Element);
instructions?: string | (() => h.JSX.Element);
copyBtn?: boolean;
onActionHandled?: (rtnObj: ActionHandledReturnObject) => void;
Expand Down
Loading