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

Add stored payment logic to PayTo #3156

Merged
merged 7 commits into from
Feb 27, 2025
Merged
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
5 changes: 5 additions & 0 deletions .changeset/yellow-pants-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adyen/adyen-web': patch
---

Adds stored component logic to PayTo
198 changes: 198 additions & 0 deletions packages/lib/src/components/PayTo/PayTo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,157 @@ describe('PayTo', () => {
expect(screen.getByLabelText(/Email/i)).toBeTruthy();
});

describe('PayTo shopperIdentifier', () => {
test('should send phoneNumber in shopperIdentifier', async () => {
const payTo = new PayTo(global.core, {
onSubmit: onSubmitMock,
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources }
});

render(payTo.render());

await user.type(screen.queryByLabelText(/Mobile number/i), '411111111');
await user.type(screen.queryByLabelText(/First name/i), 'John');
await user.type(screen.queryByLabelText(/Last name/i), 'Doe');

await user.click(screen.queryByRole('button', { name: 'Continue' }));

expect(onSubmitMock).toHaveBeenCalledTimes(1);
expect(onSubmitMock).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
paymentMethod: expect.objectContaining({
shopperAccountIdentifier: '+61-411111111',
type: 'payto'
}),
shopperName: {
firstName: 'John',
lastName: 'Doe'
}
})
}),
expect.anything(),
expect.anything()
);
});

test('should send email in shopperIdentifier', async () => {
const payTo = new PayTo(global.core, {
onSubmit: onSubmitMock,
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources }
});

render(payTo.render());

await user.click(screen.queryByRole('button', { name: 'Mobile' }));
await user.click(screen.queryByRole('option', { name: /Email/i }));

await user.type(screen.queryByLabelText(/Email/i), 'example@example.com');
await user.type(screen.queryByLabelText(/First name/i), 'John');
await user.type(screen.queryByLabelText(/Last name/i), 'Doe');

await user.click(screen.queryByRole('button', { name: 'Continue' }));

expect(onSubmitMock).toHaveBeenCalledTimes(1);
expect(onSubmitMock).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
paymentMethod: expect.objectContaining({
shopperAccountIdentifier: 'example@example.com',
type: 'payto'
}),
shopperName: {
firstName: 'John',
lastName: 'Doe'
}
})
}),
expect.anything(),
expect.anything()
);
});

test('should send ABN in shopperIdentifier', async () => {
const payTo = new PayTo(global.core, {
onSubmit: onSubmitMock,
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources }
});

render(payTo.render());

await user.click(screen.queryByRole('button', { name: 'Mobile' }));
await user.click(screen.queryByRole('option', { name: /ABN/i }));

await user.type(screen.queryByLabelText(/ABN/i), '123123123');
await user.type(screen.queryByLabelText(/First name/i), 'John');
await user.type(screen.queryByLabelText(/Last name/i), 'Doe');

await user.click(screen.queryByRole('button', { name: 'Continue' }));

expect(onSubmitMock).toHaveBeenCalledTimes(1);
expect(onSubmitMock).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
paymentMethod: expect.objectContaining({
shopperAccountIdentifier: '123123123',
type: 'payto'
}),
shopperName: {
firstName: 'John',
lastName: 'Doe'
}
})
}),
expect.anything(),
expect.anything()
);
});

test('should send BSB in shopperIdentifier', async () => {
const payTo = new PayTo(global.core, {
onSubmit: onSubmitMock,
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources }
});

render(payTo.render());

await user.click(screen.queryByRole('button', { name: /BSB and account number/i }));

await user.type(screen.queryByLabelText(/Bank account number/i), '12300123');
await user.type(screen.queryByLabelText(/Bank State Branch/i), '123456');
await user.type(screen.queryByLabelText(/First name/i), 'John');
await user.type(screen.queryByLabelText(/Last name/i), 'Doe');

await user.click(screen.queryByRole('button', { name: 'Continue' }));

expect(onSubmitMock).toHaveBeenCalledTimes(1);
expect(onSubmitMock).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
paymentMethod: expect.objectContaining({
shopperAccountIdentifier: '123456-12300123',
type: 'payto'
}),
shopperName: {
firstName: 'John',
lastName: 'Doe'
}
})
}),
expect.anything(),
expect.anything()
);
});
});

describe('PayTo await screen', () => {
const server = setupServer(
http.post('https://checkoutshopper-test.adyen.com/checkoutshopper/services/PaymentInitiation/v1/status', () => {
Expand Down Expand Up @@ -268,4 +419,51 @@ describe('PayTo', () => {
expect(mandateFrequency.nextSibling).toHaveTextContent('Ad Hoc');
});
});

describe('PayTo stored', () => {
test('should render pay button when stored', async () => {
const payTo = new PayTo(global.core, {
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources },
storedPaymentMethodId: 'mock'
});

render(payTo.render());
expect(await screen.findByRole('button', { name: /Pay/i })).toBeTruthy();
expect(screen.queryByLabelText(/Prefix/i)).toBeFalsy();
expect(screen.queryByLabelText(/Prefix/i)).toBeFalsy();
expect(screen.queryByLabelText(/Mobile number/i)).toBeFalsy();
expect(screen.queryByLabelText(/First name/i)).toBeFalsy();
expect(screen.queryByLabelText(/Last name/i)).toBeFalsy();
});

test('should send storedPaymentMethodId button when stored', async () => {
const payTo = new PayTo(global.core, {
onSubmit: onSubmitMock,
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources },
storedPaymentMethodId: 'mock'
});

render(payTo.render());

await user.click(screen.queryByRole('button', { name: /Pay/i }));

expect(onSubmitMock).toHaveBeenCalledTimes(1);
expect(onSubmitMock).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
paymentMethod: expect.objectContaining({
storedPaymentMethodId: 'mock',
type: 'payto'
})
})
}),
expect.anything(),
expect.anything()
);
});
});
});
55 changes: 49 additions & 6 deletions packages/lib/src/components/PayTo/PayTo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import PayToComponent from './components/PayToComponent';
import { PayToInstructions } from './components/PayToInstructions';
import MandateSummary from './components/MandateSummary';
import { PayToConfiguration, PayToData } from './types';
import PayButton, { payAmountLabel } from '../internal/PayButton';

/*
Await Config (previously in its own file)
Expand Down Expand Up @@ -67,6 +68,15 @@ export class PayToElement extends UIElement<PayToConfiguration> {
* Formats the component data output
*/
formatData() {
if (this.props.storedPaymentMethodId) {
return {
paymentMethod: {
type: PayToElement.type,
storedPaymentMethodId: this.props.storedPaymentMethodId
}
};
}

return {
paymentMethod: {
type: PayToElement.type,
Expand All @@ -75,16 +85,47 @@ export class PayToElement extends UIElement<PayToConfiguration> {
shopperName: {
firstName: this.state.data.firstName,
lastName: this.state.data.lastName
},
mandate: this.props.mandate
}
};
}

get isValid(): boolean {
if (this.props.storedPaymentMethodId) {
return true;
}

return !!this.state.isValid;
}

get displayName() {
if (this.props.storedPaymentMethodId && this.props.label) {
return this.props.label;
}
return this.props.name;
}

get additionalInfo() {
return this.props.storedPaymentMethodId ? this.props.name : '';
}

render() {
// Stored
if (this.props.storedPaymentMethodId) {
return (
<CoreProvider i18n={this.props.i18n} loadingContext={this.props.loadingContext} resources={this.resources}>
{this.props.showPayButton && (
<PayButton
{...this.props}
classNameModifiers={['standalone']}
amount={this.props.amount}
label={payAmountLabel(this.props.i18n, this.props.amount)}
onClick={this.submit}
/>
)}
</CoreProvider>
);
}
// Await
if (this.props.paymentData) {
return (
<CoreProvider i18n={this.props.i18n} loadingContext={this.props.loadingContext} resources={this.resources}>
Expand All @@ -108,15 +149,17 @@ export class PayToElement extends UIElement<PayToConfiguration> {
throttleTime={config.THROTTLE_TIME}
throttleInterval={config.THROTTLE_INTERVAL}
onActionHandled={this.onActionHandled}
endSlot={() => (
<MandateSummary mandate={this.props.mandate} payee={this.props.payee} currencyCode={this.props.amount.currency} />
)}
endSlot={() =>
!!this.props.mandate && (
<MandateSummary mandate={this.props.mandate} payee={this.props.payee} currencyCode={this.props.amount.currency} />
)
}
/>
</SRPanelProvider>
</CoreProvider>
);
}

// Input
return (
<CoreProvider i18n={this.props.i18n} loadingContext={this.props.loadingContext} resources={this.resources}>
<PayToComponent
Expand Down
41 changes: 19 additions & 22 deletions packages/lib/src/components/PayTo/components/PayToComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { h } from 'preact';
import SegmentedControl from '../../internal/SegmentedControl';
import { useState } from 'preact/hooks';
import { useMemo, useState } from 'preact/hooks';
import { SegmentedControlOptions } from '../../internal/SegmentedControl/SegmentedControl';
import PayIDInput from './PayIDInput';
import BSBInput from './BSBInput';
Expand All @@ -15,27 +15,6 @@ export type PayToInputOption = 'payid-option' | 'bsb-option';

export type PayToComponentData = { selectedInput: PayToInputOption };

const inputOptions: SegmentedControlOptions<PayToInputOption> = [
{
value: 'payid-option',
label: 'PayID',
htmlProps: {
id: 'payid-option', // TODO move this to i18n
'aria-controls': 'payid-input',
'aria-expanded': true // TODO move this logic to segmented controller
}
},
{
value: 'bsb-option',
label: 'BSB and account number', // TODO move this to i18n
htmlProps: {
id: 'bsb-option',
'aria-controls': 'bsb-input',
'aria-expanded': false // TODO move this logic to segmented controller
}
}
];

export interface PayToComponentProps {
showPayButton: boolean;
onChange: (e) => void;
Expand All @@ -50,6 +29,24 @@ export default function PayToComponent(props: PayToComponentProps) {

const [status, setStatus] = useState<UIElementStatus>('ready');

const inputOptions: SegmentedControlOptions<PayToInputOption> = useMemo(
() => [
{
value: 'payid-option',
label: 'PayID',
id: 'payid-option',
controls: 'payid-input'
},
{
value: 'bsb-option',
label: i18n.get('payto.bsb.option.label'),
id: 'bsb-option',
controls: 'bsb-input'
}
],
[i18n]
);

const defaultOption = inputOptions[0].value;
const [selectedInput, setSelectedInput] = useState<PayToInputOption>(defaultOption);

Expand Down
2 changes: 1 addition & 1 deletion packages/lib/src/components/PayTo/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface PayToConfiguration extends UIElementProps {
paymentData?: any;
data?: PayToData;
placeholders?: PayToPlaceholdersType;
mandate: MandateType;
mandate?: MandateType;
payee?: string;
}

Expand Down
Loading
Loading