Skip to content

Commit

Permalink
fix: Add country compatibility with BNPL Affirm at the form level (#5)
Browse files Browse the repository at this point in the history
REV-3830
  • Loading branch information
julianajlk authored Apr 23, 2024
1 parent 85e5f51 commit d71837f
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 11 deletions.
4 changes: 2 additions & 2 deletions audit-ci.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"allowlist": [
"GHSA-hpx4-r86g-5jrg",
"GHSA-wf5p-g6vw-rhxx",
"GHSA-cxjh-pqwp-8mfp",
"GHSA-wr3j-pwj9-hqq6",
"GHSA-rv95-896h-c2vc"
"GHSA-rv95-896h-c2vc",
"GHSA-8cp3-66vr-3r4c"
],
"moderate": true
}
1 change: 1 addition & 0 deletions docs/how_tos/feedback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ pre-built APIs that do not follow the format of the feedback module in the:
* `feedback/data/sagas.js`_

* ``basket-changed-error-message``
* ``dynamic-payment-methods-country-not-compatible``
* ``transaction-declined-message``
* ``error_message`` in URL parameters

Expand Down
17 changes: 11 additions & 6 deletions src/payment/PaymentPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { AppContext } from '@edx/frontend-platform/react';
import { sendPageEvent } from '@edx/frontend-platform/analytics';

import messages from './PaymentPage.messages';
import { handleApiError } from './data/handleRequestError';
import handleRequestError from './data/handleRequestError';

// Actions
import { fetchBasket } from './data/actions';
Expand Down Expand Up @@ -80,13 +80,18 @@ class PaymentPage extends React.Component {
// Get Payment Intent to retrieve the payment status and order number associated with this DPM payment.
// If this is not a Stripe dynamic payment methods (BNPL), URL will not contain any params
// and should not retrieve the Payment Intent.
// TODO: depending on if we'll use this MFE in the future, refactor to follow the redux pattern with actions
// and reducers for getting the Payment Intent is more appropriate.
const searchParams = new URLSearchParams(global.location.search);
const clientSecretId = searchParams.get('payment_intent_client_secret');
if (clientSecretId) {
const { paymentIntent, error } = await this.state.stripe.retrievePaymentIntent(clientSecretId);
if (error) { handleApiError(error); }
this.setState({ orderNumber: paymentIntent.description });
this.setState({ paymentStatus: paymentIntent.status });
try {
const { paymentIntent } = await this.state.stripe.retrievePaymentIntent(clientSecretId);
this.setState({ orderNumber: paymentIntent?.description });
this.setState({ paymentStatus: paymentIntent?.status });
} catch (error) {
handleRequestError(error);
}
}
};

Expand Down Expand Up @@ -118,7 +123,7 @@ class PaymentPage extends React.Component {
}

// If this is a redirect from Stripe Dynamic Payment Methods, show loading icon until getPaymentStatus is done.
if (isPaymentRedirect && (paymentStatus !== 'requires_payment_method' || paymentStatus !== 'canceled')) {
if (isPaymentRedirect && paymentStatus === null) {
return (
<PageLoading
srMessage={this.props.intl.formatMessage(messages['payment.loading.payment'])}
Expand Down
2 changes: 1 addition & 1 deletion src/payment/checkout/Checkout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ Checkout.propTypes = {
paymentMethod: PropTypes.oneOf(['paypal', 'apple-pay', 'cybersource', 'stripe']),
orderType: PropTypes.oneOf(Object.values(ORDER_TYPES)),
enableStripePaymentProcessor: PropTypes.bool,
stripe: PropTypes.func,
stripe: PropTypes.object, // eslint-disable-line react/forbid-prop-types
clientSecretId: PropTypes.string,
};

Expand Down
5 changes: 5 additions & 0 deletions src/payment/checkout/payment-form/PaymentForm.messages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const messages = defineMessages({
defaultMessage: 'We apologize for the inconvenience but for the time being we require ASCII characters in the name field. We are working on addressing this and appreciate your patience.',
description: 'The form field feedback text for name format issue.',
},
'payment.form.errors.dynamic_payment_methods_not_compatible.country': {
id: 'payment.form.errors.dynamic_payment_methods_not_compatible.country',
defaultMessage: 'Payment method not available for selected country',
description: 'Notifies the user their billing country is not compatible with the Dynamic Payment Method selected.',
},
});

export default messages;
11 changes: 10 additions & 1 deletion src/payment/checkout/payment-form/StripePaymentForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import MonthlyBillingNotification from '../../../subscription/checkout/monthly-b
import { Secure3DModal } from '../../../subscription/secure-3d/secure-3d-modal/Secure3dModal';

import {
getRequiredFields, validateRequiredFields, validateAsciiNames,
getRequiredFields,
validateRequiredFields,
validateAsciiNames,
validateCountryPaymentMethodCompatibility,
} from './utils/form-validators';

import { getPerformanceProperties, markPerformanceIfAble } from '../../performanceEventing';
Expand Down Expand Up @@ -117,6 +120,7 @@ const StripePaymentForm = ({
const {
firstName,
lastName,
country,
} = values;

let stripeElementErrors = null;
Expand All @@ -134,6 +138,11 @@ const StripePaymentForm = ({
firstName,
lastName,
),
...validateCountryPaymentMethodCompatibility(
isDynamicPaymentMethodsEnabled,
stripeSelectedPaymentMethod,
country,
),
};

if (Object.keys(errors).length > 0 || stripeElementErrors) {
Expand Down
23 changes: 22 additions & 1 deletion src/payment/checkout/payment-form/StripePaymentForm.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
jest.useFakeTimers('modern');

const validateRequiredFieldsMock = jest.spyOn(formValidators, 'validateRequiredFields');

const validateCountryPaymentMethodCompatibilityMock = jest.spyOn(formValidators, 'validateCountryPaymentMethodCompatibility');
const mockStore = configureMockStore();

configureI18n({
Expand Down Expand Up @@ -260,8 +260,10 @@ describe('<StripePaymentForm />', () => {
const testData = [
[
{ firstName: 'This field is required' },
{ country: 'Country not available with selected payment method' },
new SubmissionError({
firstName: 'This field is required',
country: 'Country not available with selected payment method',
}),
],
[
Expand All @@ -272,6 +274,7 @@ describe('<StripePaymentForm />', () => {

testData.forEach((testCaseData) => {
validateRequiredFieldsMock.mockReturnValueOnce(testCaseData[0]);
validateCountryPaymentMethodCompatibilityMock.mockReturnValueOnce(testCaseData[0]);
if (testCaseData[1]) {
expect(() => fireEvent.click(screen.getByText('Place Order')));
expect(submitStripePayment).not.toHaveBeenCalled();
Expand All @@ -294,4 +297,22 @@ describe('<StripePaymentForm />', () => {
expect(formValidators.validateRequiredFields(values)).toEqual(expectedErrors);
});
});

describe('validateCountryPaymentMethodCompatibility', () => {
it('returns errors if country is not compatible with Dynamic Payment Method (BNPL Affirm)', () => {
const values = {
country: 'BR',
};
const expectedErrors = {
country: 'payment.form.errors.dynamic_payment_methods_not_compatible.country',
};
const isDynamicPaymentMethodsEnabled = true;
const stripeSelectedPaymentMethod = 'affirm';
expect(formValidators.validateCountryPaymentMethodCompatibility(
isDynamicPaymentMethodsEnabled,
stripeSelectedPaymentMethod,
values.country,
)).toEqual(expectedErrors);
});
});
});
20 changes: 20 additions & 0 deletions src/payment/checkout/payment-form/utils/form-validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,26 @@ export function validateRequiredFields(values) {
return errors;
}

export function validateCountryPaymentMethodCompatibility(
isDynamicPaymentMethodsEnabled,
stripeSelectedPaymentMethod,
selectedCountry,
) {
const errors = {};

// Only adding country validation on the form level for BNPL Affirm.
// For Klarna, there is validation on the Stripe API level,
// which is handled with error code 'dynamic-payment-methods-country-not-compatible'
if (isDynamicPaymentMethodsEnabled && stripeSelectedPaymentMethod === 'affirm') {
const countryListCompatibleAffirm = ['CA', 'US'];
if (!countryListCompatibleAffirm.includes(selectedCountry)) {
errors.country = 'payment.form.errors.dynamic_payment_methods_not_compatible.country';
}
}

return errors;
}

export function validateCardDetails(cardExpirationMonth, cardExpirationYear) {
const errors = {};

Expand Down

0 comments on commit d71837f

Please sign in to comment.