Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createCheckoutService, createLanguageService } from '@bigcommerce/checkout-sdk';
import { createOfflinePaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/offline';
import { Formik } from 'formik';
import React from 'react';

import { type PaymentMethodProps } from '@bigcommerce/checkout/payment-integration-api';
import { render } from '@bigcommerce/checkout/test-utils';
import { render, screen } from '@bigcommerce/checkout/test-utils';

import OfflinePaymentMethod from './OfflinePaymentMethod';
import { getMethod } from './payment-method.mock';
Expand Down Expand Up @@ -72,4 +73,48 @@ describe('OfflinePaymentMethod', () => {

await expect(checkoutService.deinitializePayment).rejects.toThrow('test error');
});

describe('formFieldsData', () => {
const formFieldsData = [
{
name: 'Purchase Order',
id: 'purchaseOrderNumber',
required: true,
type: 'number',
fieldType: 'text',
},
];

it('returns null when formFieldsData is not present', () => {
const { container } = render(<OfflinePaymentMethod {...defaultProps} />);

expect(container).toBeEmptyDOMElement();
});

it('returns null when formFieldsData is an empty array', () => {
const props = {
...defaultProps,
method: { ...defaultProps.method, initializationData: { formFieldsData: [] } },
};

const { container } = render(<OfflinePaymentMethod {...props} />);

expect(container).toBeEmptyDOMElement();
});

it('renders fields from formFieldsData via PaymentFormFields', () => {
const props = {
...defaultProps,
method: { ...defaultProps.method, initializationData: { formFieldsData } },
};

render(
<Formik initialValues={{}} onSubmit={jest.fn()}>
<OfflinePaymentMethod {...props} />
</Formik>,
);

expect(screen.getByLabelText('Purchase Order')).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { createOfflinePaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/offline';
import { type FunctionComponent, useEffect } from 'react';
import React, { type FunctionComponent, useEffect } from 'react';

import {
getPaymentFormFields,
type PaymentMethodProps,
toResolvableComponent,
} from '@bigcommerce/checkout/payment-integration-api';

import PaymentFormFields from './PaymentFormFields';

const OfflinePaymentMethod: FunctionComponent<PaymentMethodProps> = ({
method,
checkoutService,
Expand Down Expand Up @@ -46,7 +49,13 @@ const OfflinePaymentMethod: FunctionComponent<PaymentMethodProps> = ({
};
}, [checkoutService, method.gateway, method.id, onUnhandledError]);

return null;
const formFields = getPaymentFormFields(method.initializationData?.formFieldsData);

if (!formFields.length) {
return null;
}

return <PaymentFormFields fields={formFields} />;
};

export default toResolvableComponent(OfflinePaymentMethod, [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Formik } from 'formik';
import React from 'react';

import { type PaymentFormField } from '@bigcommerce/checkout/payment-integration-api';
import { fireEvent, render, screen } from '@bigcommerce/checkout/test-utils';

import PaymentFormFields from './PaymentFormFields';

const fields: PaymentFormField[] = [
{
name: 'Purchase Order',
id: 'purchaseOrderNumber',
required: true,
type: 'number',
fieldType: 'text',
},
{
name: 'Reference',
id: 'referenceText',
required: false,
type: 'text',
fieldType: 'text',
},
];

const renderWithFormik = (ui: React.ReactElement) =>
render(
<Formik initialValues={{}} onSubmit={jest.fn()}>
{ui}
</Formik>,
);

describe('PaymentFormFields', () => {
it('renders a label and input for each field', () => {
renderWithFormik(<PaymentFormFields fields={fields} />);

expect(screen.getByLabelText('Purchase Order')).toBeInTheDocument();
expect(screen.getByLabelText('Reference')).toBeInTheDocument();
});

it('uses numeric inputMode for number type fields', () => {
renderWithFormik(<PaymentFormFields fields={fields} />);

expect(screen.getByLabelText('Purchase Order')).toHaveAttribute('inputmode', 'numeric');
expect(screen.getByLabelText('Reference')).not.toHaveAttribute('inputmode');
});

it('strips non-digit characters from a number field on change', () => {
renderWithFormik(<PaymentFormFields fields={fields} />);

const input = screen.getByLabelText('Purchase Order');

fireEvent.change(input, { target: { value: '123abc' } });

expect((input as HTMLInputElement).value).toBe('123');
});

it('shows a validation error for a required field left empty', async () => {
renderWithFormik(<PaymentFormFields fields={fields} />);

fireEvent.blur(screen.getByLabelText('Purchase Order'));

expect(await screen.findByText('Purchase Order is required')).toBeInTheDocument();
});

it('does not show a validation error for an optional field left empty', () => {
renderWithFormik(<PaymentFormFields fields={fields} />);

fireEvent.blur(screen.getByLabelText('Reference'));

expect(screen.queryByText('Reference is required')).not.toBeInTheDocument();
});
});
55 changes: 55 additions & 0 deletions packages/offline-payment-integration/src/PaymentFormFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { type FunctionComponent } from 'react';

import {
getPaymentFormFields,
type PaymentFormField,
} from '@bigcommerce/checkout/payment-integration-api';
import { BasicFormField, TextInput } from '@bigcommerce/checkout/ui';

export { getPaymentFormFields, type PaymentFormField };

const validateField = (value: string, field: PaymentFormField): string | undefined => {
if (field.required && !value) {
return `${field.name} is required`;
}

if (field.type === 'number' && value && !/^\d+$/.test(value)) {
return `${field.name} must be a positive whole number`;
}
};

const PaymentFormFields: FunctionComponent<{ fields: PaymentFormField[] }> = ({ fields }) => (
<>
{fields.map((field) => (
<BasicFormField
key={field.id}
name={field.id}
render={({ field: fieldProps, meta }) => (
<div style={{ paddingBottom: '1rem' }}>
<label htmlFor={field.id}>{field.name}</label>
<TextInput
{...fieldProps}
id={field.id}
inputMode={field.type === 'number' ? 'numeric' : undefined}
onChange={
field.type === 'number'
? (e) => {
e.target.value = e.target.value.replace(/\D/g, '');
fieldProps.onChange(e);
}
: fieldProps.onChange
}
type="text"
/>
{meta.touched && meta.error ? (
<p className="form-field-error-message">{meta.error}</p>
) : null}
</div>
)}
validate={(value: string) => validateField(value, field)}
/>
))}
</>
);

export default PaymentFormFields;
29 changes: 29 additions & 0 deletions packages/payment-integration-api/src/PaymentFormFields.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getPaymentFormFields, type PaymentFormField } from './PaymentFormFields';

const fields: PaymentFormField[] = [
{
name: 'Purchase Order',
id: 'purchaseOrderNumber',
required: true,
type: 'number',
fieldType: 'text',
},
];

describe('getPaymentFormFields', () => {
it('returns empty array when value is undefined', () => {
expect(getPaymentFormFields(undefined)).toEqual([]);
});

it('returns empty array when value is null', () => {
expect(getPaymentFormFields(null)).toEqual([]);
});

it('returns empty array when value is not an array', () => {
expect(getPaymentFormFields({ formFieldsData: [] })).toEqual([]);
});

it('returns the array when value is a valid array', () => {
expect(getPaymentFormFields(fields)).toEqual(fields);
});
});
13 changes: 13 additions & 0 deletions packages/payment-integration-api/src/PaymentFormFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface PaymentFormField {
name: string;
id: string;
required: boolean;
type: string;
fieldType: string;
}

const isPaymentFormFieldArray = (value: unknown): value is PaymentFormField[] =>
Array.isArray(value);

export const getPaymentFormFields = (formFieldsData: unknown): PaymentFormField[] =>
isPaymentFormFieldArray(formFieldsData) ? formFieldsData : [];
1 change: 1 addition & 0 deletions packages/payment-integration-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {
export { SpecificError } from './errors';
export { getCountryData } from './CountryData';
export { CaptureMessageComponent } from './CaptureMessageComponent';
export { getPaymentFormFields, type PaymentFormField } from './PaymentFormFields';

// export types separately
export type { CountryData } from './CountryData';
Expand Down