Skip to content

Commit 9171160

Browse files
committed
feat(payment): PAYMENTS-11577 Render fields coming from provider initialzation data - usage of purchase order
1 parent 0175686 commit 9171160

3 files changed

Lines changed: 156 additions & 4 deletions

File tree

dist/checkout-12d3dbb6.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/offline-payment-integration/src/OfflinePaymentMethod.test.tsx

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { createCheckoutService, createLanguageService } from '@bigcommerce/checkout-sdk';
22
import { createOfflinePaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/offline';
3+
import { Formik } from 'formik';
34
import React from 'react';
45

56
import { type PaymentMethodProps } from '@bigcommerce/checkout/payment-integration-api';
6-
import { render } from '@bigcommerce/checkout/test-utils';
7+
import { fireEvent, render, screen } from '@bigcommerce/checkout/test-utils';
78

89
import OfflinePaymentMethod from './OfflinePaymentMethod';
910
import { getMethod } from './payment-method.mock';
@@ -72,4 +73,102 @@ describe('OfflinePaymentMethod', () => {
7273

7374
await expect(checkoutService.deinitializePayment).rejects.toThrow('test error');
7475
});
76+
77+
describe('formFieldsData', () => {
78+
const formFieldsData = [
79+
{ name: 'Purchase Order', id: 'purchaseOrderNumber', required: true, type: 'number', fieldType: 'text' },
80+
{ name: 'Reference', id: 'referenceText', required: false, type: 'text', fieldType: 'text' },
81+
];
82+
83+
const renderWithFormik = (props: PaymentMethodProps) =>
84+
render(
85+
<Formik initialValues={{}} onSubmit={jest.fn()}>
86+
<OfflinePaymentMethod {...props} />
87+
</Formik>,
88+
);
89+
90+
it('returns null when formFieldsData is not present', () => {
91+
const { container } = render(<OfflinePaymentMethod {...defaultProps} />);
92+
93+
expect(container).toBeEmptyDOMElement();
94+
});
95+
96+
it('returns null when formFieldsData is an empty array', () => {
97+
const props = {
98+
...defaultProps,
99+
method: { ...defaultProps.method, initializationData: { formFieldsData: [] } },
100+
};
101+
102+
const { container } = render(<OfflinePaymentMethod {...props} />);
103+
104+
expect(container).toBeEmptyDOMElement();
105+
});
106+
107+
it('renders a field for each entry in formFieldsData', () => {
108+
const props = {
109+
...defaultProps,
110+
method: { ...defaultProps.method, initializationData: { formFieldsData } },
111+
};
112+
113+
renderWithFormik(props);
114+
115+
expect(screen.getByLabelText('Purchase Order')).toBeInTheDocument();
116+
expect(screen.getByLabelText('Reference')).toBeInTheDocument();
117+
});
118+
119+
it('strips non-digit characters from a number field on change', () => {
120+
const props = {
121+
...defaultProps,
122+
method: { ...defaultProps.method, initializationData: { formFieldsData } },
123+
};
124+
125+
renderWithFormik(props);
126+
127+
const input = screen.getByLabelText('Purchase Order');
128+
129+
fireEvent.change(input, { target: { value: '123abc' } });
130+
131+
expect((input as HTMLInputElement).value).toBe('123');
132+
});
133+
134+
it('shows a validation error for a required field left empty', async () => {
135+
const props = {
136+
...defaultProps,
137+
method: { ...defaultProps.method, initializationData: { formFieldsData } },
138+
};
139+
140+
renderWithFormik(props);
141+
142+
const input = screen.getByLabelText('Purchase Order');
143+
144+
fireEvent.blur(input);
145+
146+
expect(await screen.findByText('Purchase Order is required')).toBeInTheDocument();
147+
});
148+
149+
it('does not show a validation error for an optional field left empty', async () => {
150+
const props = {
151+
...defaultProps,
152+
method: { ...defaultProps.method, initializationData: { formFieldsData } },
153+
};
154+
155+
renderWithFormik(props);
156+
157+
fireEvent.blur(screen.getByLabelText('Reference'));
158+
159+
expect(screen.queryByText('Reference is required')).not.toBeInTheDocument();
160+
});
161+
162+
// it('uses numeric inputMode for number type fields', () => {
163+
// const props = {
164+
// ...defaultProps,
165+
// method: { ...defaultProps.method, initializationData: { formFieldsData } },
166+
// };
167+
//
168+
// renderWithFormik(props);
169+
//
170+
// expect(screen.getByLabelText('Purchase Order')).toHaveAttribute('inputmode', 'numeric');
171+
// expect(screen.getByLabelText('Reference')).not.toHaveAttribute('inputmode');
172+
// });
173+
});
75174
});

packages/offline-payment-integration/src/OfflinePaymentMethod.tsx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
11
import { createOfflinePaymentStrategy } from '@bigcommerce/checkout-sdk/integrations/offline';
2-
import { type FunctionComponent, useEffect } from 'react';
2+
import React, { type FunctionComponent, useEffect } from 'react';
33

44
import {
55
type PaymentMethodProps,
66
toResolvableComponent,
77
} from '@bigcommerce/checkout/payment-integration-api';
8+
import { BasicFormField, TextInput } from '@bigcommerce/checkout/ui';
9+
10+
interface OfflineFormField {
11+
name: string;
12+
id: string;
13+
required: boolean;
14+
type: string;
15+
fieldType: string;
16+
}
17+
18+
const validateField = (value: string, field: OfflineFormField): string | undefined => {
19+
if (field.required && !value) {
20+
return `${field.name} is required`;
21+
}
22+
23+
if (field.type === 'number' && value && (isNaN(Number(value)) || !/^\d+$/.test(value))) {
24+
return `${field.name} must be a positive whole number`;
25+
}
26+
};
827

928
const OfflinePaymentMethod: FunctionComponent<PaymentMethodProps> = ({
1029
method,
@@ -46,7 +65,41 @@ const OfflinePaymentMethod: FunctionComponent<PaymentMethodProps> = ({
4665
};
4766
}, [checkoutService, method.gateway, method.id, onUnhandledError]);
4867

49-
return null;
68+
const formFields: OfflineFormField[] = method.initializationData?.formFieldsData ?? [];
69+
70+
if (!formFields.length) {
71+
return null;
72+
}
73+
74+
return (
75+
<>
76+
{formFields.map((field) => (
77+
<BasicFormField
78+
key={field.id}
79+
name={field.id}
80+
validate={(value: string) => validateField(value, field)}
81+
render={({ field: fieldProps, meta }) => (
82+
<div style={{ paddingBottom: '1rem' }}>
83+
<label htmlFor={field.id}>{field.name}</label>
84+
<TextInput
85+
{...fieldProps}
86+
id={field.id}
87+
type="text"
88+
inputMode={field.type === 'number' ? 'numeric' : undefined}
89+
onChange={field.type === 'number' ? (e) => {
90+
e.target.value = e.target.value.replace(/\D/g, '');
91+
fieldProps.onChange(e);
92+
} : fieldProps.onChange}
93+
/>
94+
{meta.touched && meta.error && (
95+
<p className="form-field-error-message">{meta.error}</p>
96+
)}
97+
</div>
98+
)}
99+
/>
100+
))}
101+
</>
102+
);
50103
};
51104

52105
export default toResolvableComponent(OfflinePaymentMethod, [

0 commit comments

Comments
 (0)