Skip to content

Commit

Permalink
Calling actions.reject() in beforeSubmit should not lead to onPayment…
Browse files Browse the repository at this point in the history
…Failed (#2901)

* Calling actions.reject() in beforeSubmit should not lead to onPaymentFailed being called

* Calling actions.reject() in beforeSubmit - second approach: not touching handleFailedResult

* Restored handleFailedResult to original state

* Introduced new CancelError class and removed .catch on beforeSubmitEvent promise

* Added changeset file

* Adding unit tests

* Streamlining tests

* Refactor tests to use mock element rather than Card
  • Loading branch information
sponglord authored Oct 18, 2024
1 parent 8b9aa12 commit afb4c53
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-balloons-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adyen/adyen-web': patch
---

Calling actions.reject() in the beforeSubmit callback should leave the UI in the current state. Fixes situation where it leads to a call to handleFailedResult which ultimately leads to a call to the onPaymentFailed callback and sets the UI to an error state
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { h } from 'preact';
import UIElement from './UIElement';

class MyElement extends UIElement {
public static type = 'myComp';

// @ts-ignore it's a test
public elementRef = {
state: {
status: null
},
setStatus: status => {
this.elementRef.state.status = status;
}
};

get isValid() {
return !!this.state.isValid;
}

render() {
return '';
}
}

let onPaymentCompleted;
let onPaymentFailed;

beforeEach(() => {
onPaymentCompleted = jest.fn();
onPaymentFailed = jest.fn();
});

describe('Testing beforeSubmit', () => {
test('Because beforeSubmit resolves onPaymentCompleted is called', done => {
const beforeSubmit = jest.fn((data, component, actions) => {
actions.resolve(data);
});

const myElement = new MyElement(global.core, {
amount: { value: 0, currency: 'USD' },
// @ts-ignore it's just a test
session: { configuration: {} },
beforeSubmit,
onPaymentCompleted
});

const paymentResponse = {
resultCode: 'Authorised',
sessionData: 'Ab02b4c0uKS50x...',
sessionResult: 'X3XtfGC9...'
};

// @ts-ignore it's a test
myElement.submitUsingSessionsFlow = () => {
return Promise.resolve(paymentResponse);
};

myElement.setState({ isValid: true });

myElement.submit();

// initially submit should lead to UIElement.makePaymentsCall() which sets status to 'loading'
expect(myElement.elementRef.state.status).toEqual('loading');

setTimeout(() => {
expect(beforeSubmit).toHaveBeenCalled();

// state.status doesn't get reset
expect(myElement.elementRef.state.status).toEqual('loading');

expect(onPaymentCompleted).toHaveBeenCalledWith(paymentResponse, myElement.elementRef);

done();
}, 0);
});

test('Because beforeSubmit rejects the status gets set back to "ready" but onPaymentFailed does not get called', done => {
const beforeSubmit = jest.fn((data, component, actions) => {
actions.reject();
});

const myElement = new MyElement(global.core, {
amount: { value: 0, currency: 'USD' },
// @ts-ignore it's just a test
session: { configuration: {} },
beforeSubmit,
onPaymentCompleted,
onPaymentFailed
});

myElement.setState({ isValid: true });

myElement.submit();

// initially submit should lead to UIElement.makePaymentsCall() which sets status to 'loading'
expect(myElement.elementRef.state.status).toEqual('loading');

setTimeout(() => {
expect(beforeSubmit).toHaveBeenCalled();

expect(myElement.elementRef.state.status).toEqual('ready');

expect(onPaymentCompleted).not.toHaveBeenCalled();
expect(onPaymentFailed).not.toHaveBeenCalled();

done();
}, 0);
});
});
17 changes: 14 additions & 3 deletions packages/lib/src/components/internal/UIElement/UIElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
} from '../../../types/global-types';
import type { IDropin } from '../../Dropin/types';
import type { NewableComponent } from '../../../core/core.registry';
import CancelError from '../../../core/Errors/CancelError';

import './UIElement.scss';

Expand Down Expand Up @@ -175,7 +176,17 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten
return;
}

this.makePaymentsCall().then(sanitizeResponse).then(verifyPaymentDidNotFail).then(this.handleResponse).catch(this.handleFailedResult);
this.makePaymentsCall()
.then(sanitizeResponse)
.then(verifyPaymentDidNotFail)
.then(this.handleResponse)
.catch((e: PaymentResponseData | Error) => {
if (e instanceof CancelError) {
this.setElementStatus('ready');
return;
}
this.handleFailedResult(e as PaymentResponseData);
});
}

protected makePaymentsCall(): Promise<CheckoutAdvancedFlowResponse | CheckoutSessionPaymentResponse> {
Expand All @@ -190,7 +201,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten
? new Promise((resolve, reject) =>
this.props.beforeSubmit(this.data, this.elementRef, {
resolve,
reject
reject: () => reject(new CancelError('beforeSubmitRejected'))
})
)
: Promise.resolve(this.data);
Expand Down Expand Up @@ -362,7 +373,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten
};

/**
* Handles a session /payments or /payments/details response.
* Handles a /payments or /payments/details response.
* The component will handle automatically actions, orders, and final results.
*
* @param rawResponse -
Expand Down
7 changes: 7 additions & 0 deletions packages/lib/src/core/Errors/CancelError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class CancelError extends Error {
constructor(message?: string) {
super(message);
}
}

export default CancelError;

0 comments on commit afb4c53

Please sign in to comment.