@infosoftas/s4-orders-js-sdk 0.0.1-alpha2 → 0.0.1-alpha2b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infosoftas/s4-orders-js-sdk",
3
- "version": "0.0.1-alpha2",
3
+ "version": "0.0.1-alpha2b",
4
4
  "scripts": {
5
5
  "build": "rimraf lib && webpack --env mode=production"
6
6
  },
package/src/App.tsx CHANGED
@@ -1,8 +1,14 @@
1
1
  import { FC, useEffect, useState } from 'react';
2
2
 
3
- import { MessageEventTypeEnum, PaymentMethodEnum } from './enums/general';
3
+ import {
4
+ FormTypeEnum,
5
+ MessageEventTypeEnum,
6
+ PaymentMethodEnum,
7
+ } from './enums/general';
4
8
  import useMessageEvent from './hooks/useMessageEvent';
5
9
  import OrderForm from './components/OrderForm/OrderForm';
10
+ import OIOForm from './components/OIOForm/OIOForm';
11
+ import EHFForm from './components/EHFForm/EHFForm';
6
12
  import Loader from './components/Loader/Loader';
7
13
  import MainIframe from './components/MainIframe/MainIframe';
8
14
  import Alert from './components/Alert/Alert';
@@ -27,6 +33,7 @@ const App: FC<ConfigType> = ({
27
33
  redirectUrl,
28
34
  showIframe,
29
35
  paymentMethodsOptions,
36
+ invoiceAddressSelection,
30
37
  availablePaymentMethods = [],
31
38
  language = 'en-US',
32
39
  merchantAgreementUrl = '',
@@ -34,6 +41,11 @@ const App: FC<ConfigType> = ({
34
41
  successText: 'Order successful completed!',
35
42
  failureText: 'Something went wrong!',
36
43
  buttonText: 'Start',
44
+ submitButtonText: 'Start',
45
+ backButtonText: 'Back',
46
+ verifyButtonText: 'Verify',
47
+ organizationNumberLabel: 'CVR',
48
+ glnLabel: 'GLN',
37
49
  },
38
50
  }) => {
39
51
  const [loading, setLoading] = useState<boolean>(false);
@@ -45,6 +57,7 @@ const App: FC<ConfigType> = ({
45
57
  const [failedMsg, setFailedMsg] = useState<string>('');
46
58
  const [errorsMsg, setErrorsMsg] = useState<string[]>([]);
47
59
  const [orderInfo, setOrderInfo] = useState<OrderInfoType | null>(null);
60
+ const [formType, setFormType] = useState(FormTypeEnum.ORDER);
48
61
 
49
62
  useEffect(() => {
50
63
  if (apiKey) {
@@ -107,6 +120,7 @@ const App: FC<ConfigType> = ({
107
120
  setIsFailed(true);
108
121
  setLoading(false);
109
122
  setShowOrderForm(true);
123
+ setFormType(FormTypeEnum.ORDER);
110
124
  }
111
125
  };
112
126
 
@@ -190,18 +204,15 @@ const App: FC<ConfigType> = ({
190
204
  data?.paymentMethod === PaymentMethodEnum.Email ||
191
205
  data?.paymentMethod === PaymentMethodEnum.Invoice
192
206
  ) {
193
- if (window === top) {
194
- setIsConfirmed(true);
195
- setLoading(false);
196
- top.postMessage(
197
- {
198
- type: MessageEventTypeEnum.ORDER_FLOW_COMPLETE,
199
- isCompleted: true,
200
- orderInfo: data || null,
201
- },
202
- top?.location?.origin || '*'
203
- );
204
- }
207
+ messageCallback({
208
+ orderId: data.orderId || orderId || '',
209
+ agreementId: '',
210
+ orderInfo: data || orderInfo || null,
211
+ });
212
+ } else if (data?.paymentMethod === PaymentMethodEnum.EHF) {
213
+ setFormType(FormTypeEnum.EHF);
214
+ } else if (data?.paymentMethod === PaymentMethodEnum.OIO) {
215
+ setFormType(FormTypeEnum.OIO);
205
216
  } else if (url) {
206
217
  if (
207
218
  showIframe &&
@@ -218,6 +229,18 @@ const App: FC<ConfigType> = ({
218
229
  setIframeSrc(null);
219
230
  };
220
231
 
232
+ const handleInvoiceBack = () => {
233
+ setFormType(FormTypeEnum.ORDER);
234
+ };
235
+
236
+ const handleInvoiceForm = async () => {
237
+ await messageCallback({
238
+ orderId: orderId || '',
239
+ agreementId: '',
240
+ orderInfo: orderInfo || null,
241
+ });
242
+ };
243
+
221
244
  if (window !== top) {
222
245
  return null;
223
246
  }
@@ -237,26 +260,67 @@ const App: FC<ConfigType> = ({
237
260
  </div>
238
261
  )}
239
262
  {!iframeSrc && showOrderForm && (
240
- <OrderForm
241
- callback={handleForm}
242
- templatePackageId={templatePackageId}
243
- subscriberId={subscriberId}
244
- userId={userId}
245
- identityProviderId={identityProviderId}
246
- organizationId={organizationId}
247
- paymentMethods={availablePaymentMethods}
248
- buttonText={settings?.buttonText}
249
- paymentMethodLabel={settings?.paymentMethodLabel}
250
- errorReqMsg={settings?.errorReqMsg}
251
- errorInvalidEmailMsg={settings?.errorInvalidEmailMsg}
252
- errorInvalidPhoneMsg={settings?.errorInvalidPhoneMsg}
253
- defaultValues={settings?.orderDefaultValues}
254
- redirectUrl={redirectUrl}
255
- showIframe={showIframe}
256
- paymentMethodsOptions={paymentMethodsOptions}
257
- language={language}
258
- merchantAgreementUrl={merchantAgreementUrl}
259
- />
263
+ <>
264
+ {formType === FormTypeEnum.OIO && (
265
+ <OIOForm
266
+ onBack={handleInvoiceBack}
267
+ callback={handleInvoiceForm}
268
+ backButtonText={settings?.backButtonText}
269
+ verifyButtonText={settings?.verifyButtonText}
270
+ organizationNumberLabel={
271
+ settings?.organizationNumberLabel
272
+ }
273
+ glnLabel={settings?.glnLabel}
274
+ />
275
+ )}
276
+ {formType === FormTypeEnum.EHF && (
277
+ <EHFForm
278
+ onBack={handleInvoiceBack}
279
+ callback={handleInvoiceForm}
280
+ backButtonText={settings?.backButtonText}
281
+ verifyButtonText={settings?.verifyButtonText}
282
+ organizationNumberLabel={
283
+ settings?.organizationNumberLabel
284
+ }
285
+ />
286
+ )}
287
+ {formType !== FormTypeEnum.OIO &&
288
+ formType !== FormTypeEnum.EHF && (
289
+ <OrderForm
290
+ callback={handleForm}
291
+ templatePackageId={templatePackageId}
292
+ subscriberId={subscriberId}
293
+ userId={userId}
294
+ identityProviderId={identityProviderId}
295
+ organizationId={organizationId}
296
+ paymentMethods={availablePaymentMethods}
297
+ submitButtonText={
298
+ settings?.submitButtonText
299
+ }
300
+ paymentMethodLabel={
301
+ settings?.paymentMethodLabel
302
+ }
303
+ errorReqMsg={settings?.errorReqMsg}
304
+ errorInvalidEmailMsg={
305
+ settings?.errorInvalidEmailMsg
306
+ }
307
+ errorInvalidPhoneMsg={
308
+ settings?.errorInvalidPhoneMsg
309
+ }
310
+ defaultValues={settings?.orderDefaultValues}
311
+ redirectUrl={redirectUrl}
312
+ showIframe={showIframe}
313
+ paymentMethodsOptions={
314
+ paymentMethodsOptions
315
+ }
316
+ language={language}
317
+ merchantAgreementUrl={merchantAgreementUrl}
318
+ invoiceAddressSelection={
319
+ invoiceAddressSelection
320
+ }
321
+ />
322
+ )}
323
+ </>
260
324
  )}
261
325
  {showIframe && <MainIframe iframeSrc={iframeSrc} />}
262
326
  {isConfirmed && (
@@ -0,0 +1,25 @@
1
+ import fetcher from '../utils/fetcher';
2
+
3
+ type RequestInvoiceLookupType = {
4
+ network: string;
5
+ value: string;
6
+ };
7
+
8
+ type InvoiceLookupResponseType = {
9
+ recievesInvoice: boolean;
10
+ };
11
+
12
+ export const invoiceLookup = (
13
+ body: RequestInvoiceLookupType
14
+ ): Promise<InvoiceLookupResponseType> =>
15
+ fetcher<InvoiceLookupResponseType, RequestInvoiceLookupType>({
16
+ method: 'POST',
17
+ url: '/invoicelookup',
18
+ headers: {
19
+ 'S4-ORDERS-API-KEY':
20
+ sessionStorage.getItem('sdk_api_key') ||
21
+ process.env.API_KEY ||
22
+ '',
23
+ },
24
+ body,
25
+ }) as Promise<InvoiceLookupResponseType>;
@@ -10,6 +10,7 @@ type RequestOrderStartType = {
10
10
  };
11
11
  paymentAgreement: AgreementsType;
12
12
  invoiceContact?: CreateSubscriberRequestType;
13
+ orderReference?: string;
13
14
  };
14
15
 
15
16
  type OrderResponseType = {
@@ -102,4 +102,63 @@
102
102
  }
103
103
  }
104
104
  }
105
+ }
106
+
107
+ .sdk-on-off-label {
108
+ margin-right: 0.5rem;
109
+ }
110
+
111
+ .sdk-on-off {
112
+ position: relative;
113
+ display : inline-block;
114
+ width : 60px;
115
+ height : 28px;
116
+
117
+ input {
118
+ opacity: 0;
119
+ width : 0;
120
+ height : 0;
121
+ }
122
+
123
+ input:checked+.slider:before {
124
+ -webkit-transform: translateX(32px);
125
+ -ms-transform : translateX(32px);
126
+ transform : translateX(32px);
127
+ }
128
+
129
+ input:checked+.slider {
130
+ background-color: $primary-color;
131
+ }
132
+
133
+ .slider {
134
+ position : absolute;
135
+ cursor : pointer;
136
+ top : 0;
137
+ left : 0;
138
+ right : 0;
139
+ bottom : 0;
140
+ background-color : $input-border-disabled-color;
141
+ -webkit-transition: .4s;
142
+ transition : .4s;
143
+
144
+ &:before {
145
+ position : absolute;
146
+ content : "";
147
+ height : 24px;
148
+ width : 24px;
149
+ left : 2px;
150
+ bottom : 2px;
151
+ background-color : white;
152
+ -webkit-transition: .4s;
153
+ transition : .4s;
154
+ }
155
+
156
+ &.round {
157
+ border-radius: 28px;
158
+
159
+ &:before {
160
+ border-radius: 50%;
161
+ }
162
+ }
163
+ }
105
164
  }
@@ -9,10 +9,18 @@
9
9
  justify-content: center;
10
10
  }
11
11
 
12
+ .flex-wrap {
13
+ flex-wrap: wrap;
14
+ }
15
+
12
16
  .align-center {
13
17
  align-items: center;
14
18
  }
15
19
 
20
+ .gap-2 {
21
+ gap: 0.5rem
22
+ }
23
+
16
24
  .mt-2 {
17
25
  margin-top: 0.5rem;
18
26
  }
@@ -7,7 +7,7 @@ import './button.scss';
7
7
  type Props = {
8
8
  onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
9
9
  type?: 'button' | 'submit' | 'reset';
10
- btnType?: 'primary';
10
+ btnType?: 'primary' | 'default';
11
11
  buttonText?: string;
12
12
  loading?: boolean;
13
13
  disable?: boolean;
@@ -0,0 +1,125 @@
1
+ import { FC, useState } from 'react';
2
+ import { FormProvider, useForm, SubmitHandler } from 'react-hook-form';
3
+
4
+ import { invoiceLookup } from '../../api/InvoiceApi';
5
+ import Alert from '../Alert/Alert';
6
+ import Button from '../Button/Button';
7
+ import InputField from '../FormFields/InputField';
8
+ import {
9
+ prepareErrorMessage,
10
+ prepareErrorsArrayMessage,
11
+ } from '../../utils/helper';
12
+ import { InvoiceLookupNetworkEnum } from '../../enums/general';
13
+ import { ErrorsMsg } from '../../types/general';
14
+
15
+ type Props = {
16
+ callback: (value: string) => void;
17
+ onBack: () => void;
18
+ backButtonText?: string;
19
+ verifyButtonText?: string;
20
+ organizationNumberLabel?: string;
21
+ className?: string;
22
+ };
23
+
24
+ type EHFFormInputsType = {
25
+ organizationNumber: string;
26
+ };
27
+
28
+ const initialData = {
29
+ organizationNumber: '',
30
+ };
31
+
32
+ const EHFForm: FC<Props> = ({
33
+ callback,
34
+ onBack,
35
+ backButtonText = '',
36
+ verifyButtonText = '',
37
+ organizationNumberLabel = '',
38
+ className = '',
39
+ }) => {
40
+ const [loading, setLoading] = useState<boolean>(false);
41
+ const [apiErrorMsg, setApiErrorMsg] = useState<string>('');
42
+ const [errorsMsg, setErrorsMsg] = useState<string[]>([]);
43
+
44
+ const methods = useForm<EHFFormInputsType>({
45
+ defaultValues: {
46
+ ...initialData,
47
+ },
48
+ });
49
+
50
+ const {
51
+ handleSubmit,
52
+ formState: { errors },
53
+ } = methods;
54
+
55
+ const onSubmit: SubmitHandler<EHFFormInputsType> = async (
56
+ data
57
+ ): Promise<void> => {
58
+ setApiErrorMsg('');
59
+ setErrorsMsg([]);
60
+ setLoading(true);
61
+ try {
62
+ await invoiceLookup({
63
+ network: InvoiceLookupNetworkEnum.EHF,
64
+ value: data.organizationNumber,
65
+ });
66
+ callback(data.organizationNumber);
67
+ } catch (error) {
68
+ setApiErrorMsg(prepareErrorMessage(error as Error));
69
+ setErrorsMsg(
70
+ prepareErrorsArrayMessage(
71
+ (
72
+ error as {
73
+ errors: ErrorsMsg;
74
+ }
75
+ )?.errors
76
+ )
77
+ );
78
+ } finally {
79
+ setLoading(false);
80
+ }
81
+ };
82
+
83
+ const handleBack = () => {
84
+ onBack?.();
85
+ };
86
+
87
+ return (
88
+ <FormProvider {...methods}>
89
+ <form
90
+ className={`${className}`}
91
+ onSubmit={handleSubmit(onSubmit)}
92
+ data-testid="ehf-form-id"
93
+ >
94
+ <InputField
95
+ name="organizationNumber"
96
+ label={organizationNumberLabel}
97
+ required
98
+ readOnly={false}
99
+ errors={errors}
100
+ />
101
+ <div className="d-flex justify-center flex-wrap gap-2">
102
+ <Button
103
+ type="button"
104
+ btnType="default"
105
+ disable={loading}
106
+ buttonText={backButtonText}
107
+ onClick={handleBack}
108
+ />
109
+
110
+ <Button
111
+ type="submit"
112
+ loading={loading}
113
+ buttonText={verifyButtonText}
114
+ />
115
+ </div>
116
+
117
+ {apiErrorMsg && <Alert className="mt-2" msg={apiErrorMsg} />}
118
+ {errorsMsg?.length > 0 &&
119
+ errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
120
+ </form>
121
+ </FormProvider>
122
+ );
123
+ };
124
+
125
+ export default EHFForm;
@@ -10,13 +10,14 @@ type Props = {
10
10
  label?: string;
11
11
  errorReqMsg?: string;
12
12
  errors?: {
13
- address?: {
13
+ [key: string]: {
14
14
  message?: string;
15
15
  };
16
16
  };
17
17
  };
18
18
 
19
19
  const AddressField: FC<Props> = ({
20
+ name = 'address',
20
21
  required = false,
21
22
  readOnly = false,
22
23
  label = 'Address',
@@ -33,17 +34,15 @@ const AddressField: FC<Props> = ({
33
34
  placeholder=" "
34
35
  readOnly={readOnly}
35
36
  className={`input-control ${readOnly ? 'read-only' : ''}`}
36
- {...register('address', {
37
+ {...register(name, {
37
38
  required: required ? errorReqMsg : false,
38
39
  })}
39
40
  />
40
41
  <label className="label-control" htmlFor={id}>
41
42
  {label} {required && <span className="text-error">*</span>}
42
43
  </label>
43
- {errors && errors.address && (
44
- <div className="text-error caption">
45
- {errors.address.message}
46
- </div>
44
+ {errors && errors[name] && (
45
+ <div className="text-error caption">{errors[name].message}</div>
47
46
  )}
48
47
  </div>
49
48
  );
@@ -11,13 +11,14 @@ type Props = {
11
11
  errorReqMsg?: string;
12
12
  errorInvalidEmailMsg?: string;
13
13
  errors?: {
14
- email?: {
14
+ [key: string]: {
15
15
  message?: string;
16
16
  };
17
17
  };
18
18
  };
19
19
 
20
20
  const EmailField: FC<Props> = ({
21
+ name = 'email',
21
22
  required = false,
22
23
  readOnly = false,
23
24
  label = 'Email',
@@ -36,7 +37,7 @@ const EmailField: FC<Props> = ({
36
37
  className={`input-control ${readOnly ? 'read-only' : ''}`}
37
38
  readOnly={readOnly}
38
39
  type="email"
39
- {...register('email', {
40
+ {...register(name, {
40
41
  required: required ? errorReqMsg : false,
41
42
  pattern: {
42
43
  value: /^[-!#-'*+/-9=?^-~]+(?:\.[-!#-'*+/-9=?^-~]+)*@[-!#-'*+/-9=?^-~]+(?:\.[-!#-'*+/-9=?^-~]{2,20})+$/i,
@@ -47,8 +48,8 @@ const EmailField: FC<Props> = ({
47
48
  <label className="label-control" htmlFor={id}>
48
49
  {label} {required && <span className="text-error">*</span>}
49
50
  </label>
50
- {errors && errors.email && (
51
- <div className="text-error caption">{errors.email.message}</div>
51
+ {errors && errors[name] && (
52
+ <div className="text-error caption">{errors[name].message}</div>
52
53
  )}
53
54
  </div>
54
55
  );
@@ -12,6 +12,15 @@ export const FIELD_TYPES = {
12
12
  ORGANIZATION_NUMBER: 'organizationNumber',
13
13
  BUYER_REFERENCE: 'buyerReference',
14
14
  ORDER_REFERENCE: 'orderReference',
15
+ INVOICE_NAME: 'invoiceName',
16
+ INVOICE_ADDRESS: 'invoiceAddress',
17
+ INVOICE_CITY: 'invoiceCity',
18
+ INVOICE_ZIP: 'invoiceZip',
19
+ INVOICE_COUNTRY: 'invoiceCountry',
20
+ INVOICE_PHONE: 'invoicePhoneNumber',
21
+ INVOICE_EMAIL: 'invoiceEmail',
22
+ INVOICE_CARE_OF: 'invoiceCareOf',
23
+ INVOICE_BUYER_REFERENCE: 'invoiceBuyerReference',
15
24
  };
16
25
 
17
26
  export const DEFAULT_ORDER_FORM_FIELDS: OrderFormFiledType[] = [
@@ -18,6 +18,15 @@ const formFieldsMapper = {
18
18
  [FIELD_TYPES.ORGANIZATION_NUMBER]: InputField,
19
19
  [FIELD_TYPES.BUYER_REFERENCE]: InputField,
20
20
  [FIELD_TYPES.ORDER_REFERENCE]: InputField,
21
+ [FIELD_TYPES.INVOICE_PHONE]: PhoneField,
22
+ [FIELD_TYPES.INVOICE_NAME]: NameField,
23
+ [FIELD_TYPES.INVOICE_EMAIL]: EmailField,
24
+ [FIELD_TYPES.INVOICE_ADDRESS]: AddressField,
25
+ [FIELD_TYPES.INVOICE_COUNTRY]: InputField,
26
+ [FIELD_TYPES.INVOICE_CITY]: InputField,
27
+ [FIELD_TYPES.INVOICE_ZIP]: InputField,
28
+ [FIELD_TYPES.INVOICE_CARE_OF]: InputField,
29
+ [FIELD_TYPES.INVOICE_BUYER_REFERENCE]: InputField,
21
30
  };
22
31
 
23
32
  export default formFieldsMapper;
@@ -10,13 +10,14 @@ type Props = {
10
10
  label?: string;
11
11
  errorReqMsg?: string;
12
12
  errors?: {
13
- name?: {
13
+ [key: string]: {
14
14
  message?: string;
15
15
  };
16
16
  };
17
17
  };
18
18
 
19
19
  const NameField: FC<Props> = ({
20
+ name = 'name',
20
21
  required = false,
21
22
  readOnly = false,
22
23
  label = 'Name',
@@ -33,15 +34,15 @@ const NameField: FC<Props> = ({
33
34
  placeholder=" "
34
35
  readOnly={readOnly}
35
36
  className={`input-control ${readOnly ? 'read-only' : ''}`}
36
- {...register('name', {
37
+ {...register(name, {
37
38
  required: required ? errorReqMsg : false,
38
39
  })}
39
40
  />
40
41
  <label className="label-control" htmlFor={id}>
41
42
  {label} {required && <span className="text-error">*</span>}
42
43
  </label>
43
- {errors && errors.name && (
44
- <div className="text-error caption">{errors.name.message}</div>
44
+ {errors && errors[name] && (
45
+ <div className="text-error caption">{errors[name].message}</div>
45
46
  )}
46
47
  </div>
47
48
  );
@@ -11,13 +11,14 @@ type Props = {
11
11
  errorReqMsg?: string;
12
12
  errorInvalidPhoneMsg?: string;
13
13
  errors?: {
14
- phoneNumber?: {
14
+ [key: string]: {
15
15
  message?: string;
16
16
  };
17
17
  };
18
18
  };
19
19
 
20
20
  const PhoneFiled: FC<Props> = ({
21
+ name = 'phoneNumber',
21
22
  required = false,
22
23
  readOnly = false,
23
24
  label = 'Phone number',
@@ -36,7 +37,7 @@ const PhoneFiled: FC<Props> = ({
36
37
  placeholder=" "
37
38
  readOnly={readOnly}
38
39
  type="tel"
39
- {...register('phoneNumber', {
40
+ {...register(name, {
40
41
  required: required ? errorReqMsg : false,
41
42
  pattern: {
42
43
  value: /^[+]*[(]{0,1}[0-9]{1,3}[)]{0,1}[-\s\./0-9]{6,14}$/g,
@@ -47,10 +48,8 @@ const PhoneFiled: FC<Props> = ({
47
48
  <label className="label-control" htmlFor={id}>
48
49
  {label} {required && <span className="text-error">*</span>}
49
50
  </label>
50
- {errors && errors.phoneNumber && (
51
- <div className="text-error caption">
52
- {errors.phoneNumber.message}
53
- </div>
51
+ {errors && errors[name] && (
52
+ <div className="text-error caption">{errors[name].message}</div>
54
53
  )}
55
54
  </div>
56
55
  );
@@ -0,0 +1,58 @@
1
+ import { FC } from 'react';
2
+ import { useFormContext } from 'react-hook-form';
3
+
4
+ import { uuidv4 } from '../../utils/helper';
5
+
6
+ type Props = {
7
+ name: string;
8
+ required?: boolean;
9
+ readOnly?: boolean;
10
+ label?: string;
11
+ errorReqMsg?: string;
12
+ errors?: {
13
+ [key: string]: {
14
+ message?: string;
15
+ };
16
+ };
17
+ };
18
+
19
+ const ToggleField: FC<Props> = ({
20
+ name,
21
+ required = false,
22
+ readOnly = false,
23
+ label = '',
24
+ errorReqMsg = 'This field is required!',
25
+ errors,
26
+ }) => {
27
+ const { register } = useFormContext();
28
+ const id = uuidv4();
29
+ return (
30
+ <div className="field-wrapper">
31
+ <div className="d-flex align-center flex-wrap">
32
+ <label className="sdk-on-off-label" htmlFor={id}>
33
+ {label} {required && <span className="text-error">*</span>}
34
+ </label>
35
+ <label
36
+ className="sdk-on-off"
37
+ data-testid={`sdk-${name}-field-id`}
38
+ >
39
+ <input
40
+ id={id}
41
+ type="checkbox"
42
+ readOnly={readOnly}
43
+ className={`${readOnly ? 'read-only' : ''}`}
44
+ {...register(name, {
45
+ required: required ? errorReqMsg : false,
46
+ })}
47
+ />
48
+ <span className="slider round"></span>
49
+ </label>
50
+ </div>
51
+ {errors && errors[name] && (
52
+ <div className="text-error caption">{errors[name].message}</div>
53
+ )}
54
+ </div>
55
+ );
56
+ };
57
+
58
+ export default ToggleField;
@@ -0,0 +1,141 @@
1
+ import { FC, useState } from 'react';
2
+ import { FormProvider, useForm, SubmitHandler } from 'react-hook-form';
3
+
4
+ import { invoiceLookup } from '../../api/InvoiceApi';
5
+ import Alert from '../Alert/Alert';
6
+ import Button from '../Button/Button';
7
+ import InputField from '../FormFields/InputField';
8
+ import {
9
+ prepareErrorMessage,
10
+ prepareErrorsArrayMessage,
11
+ } from '../../utils/helper';
12
+ import { InvoiceLookupNetworkEnum } from '../../enums/general';
13
+ import { ErrorsMsg } from '../../types/general';
14
+
15
+ type Props = {
16
+ callback: (value: string) => void;
17
+ onBack: () => void;
18
+ backButtonText?: string;
19
+ verifyButtonText?: string;
20
+ organizationNumberLabel?: string;
21
+ glnLabel?: string;
22
+ className?: string;
23
+ };
24
+
25
+ type OIOFormInputsType = {
26
+ cvr: string;
27
+ gln: string;
28
+ };
29
+
30
+ const initialData = {
31
+ cvr: '',
32
+ gln: '',
33
+ };
34
+
35
+ const EHFForm: FC<Props> = ({
36
+ callback,
37
+ onBack,
38
+ backButtonText = '',
39
+ verifyButtonText = '',
40
+ organizationNumberLabel = '',
41
+ glnLabel = '',
42
+ className = '',
43
+ }) => {
44
+ const [loading, setLoading] = useState<boolean>(false);
45
+ const [apiErrorMsg, setApiErrorMsg] = useState<string>('');
46
+ const [errorsMsg, setErrorsMsg] = useState<string[]>([]);
47
+
48
+ const methods = useForm<OIOFormInputsType>({
49
+ defaultValues: {
50
+ ...initialData,
51
+ },
52
+ });
53
+
54
+ const {
55
+ handleSubmit,
56
+ watch,
57
+ formState: { errors },
58
+ } = methods;
59
+
60
+ const cvr = watch('cvr');
61
+ const gln = watch('gln');
62
+
63
+ const onSubmit: SubmitHandler<OIOFormInputsType> = async (
64
+ data
65
+ ): Promise<void> => {
66
+ setApiErrorMsg('');
67
+ setErrorsMsg([]);
68
+ setLoading(true);
69
+ try {
70
+ await invoiceLookup({
71
+ network: data.gln
72
+ ? InvoiceLookupNetworkEnum.OIO_GLN
73
+ : InvoiceLookupNetworkEnum.OIO_Danish_CVR,
74
+ value: data.gln || data.cvr,
75
+ });
76
+ callback(data.gln || data.cvr);
77
+ } catch (error) {
78
+ setApiErrorMsg(prepareErrorMessage(error as Error));
79
+ setErrorsMsg(
80
+ prepareErrorsArrayMessage(
81
+ (
82
+ error as {
83
+ errors: ErrorsMsg;
84
+ }
85
+ )?.errors
86
+ )
87
+ );
88
+ } finally {
89
+ setLoading(false);
90
+ }
91
+ };
92
+
93
+ const handleBack = () => {
94
+ onBack?.();
95
+ };
96
+
97
+ return (
98
+ <FormProvider {...methods}>
99
+ <form
100
+ className={`${className}`}
101
+ onSubmit={handleSubmit(onSubmit)}
102
+ data-testid="oio-form-id"
103
+ >
104
+ <InputField
105
+ name="cvr"
106
+ label={organizationNumberLabel}
107
+ required={!gln}
108
+ readOnly={false}
109
+ errors={errors}
110
+ />
111
+ <InputField
112
+ name="gln"
113
+ label={glnLabel}
114
+ required={!cvr}
115
+ readOnly={false}
116
+ errors={errors}
117
+ />
118
+ <div className="d-flex justify-center flex-wrap gap-2">
119
+ <Button
120
+ type="button"
121
+ btnType="default"
122
+ disable={loading}
123
+ buttonText={backButtonText}
124
+ onClick={handleBack}
125
+ />
126
+
127
+ <Button
128
+ type="submit"
129
+ loading={loading}
130
+ buttonText={verifyButtonText}
131
+ />
132
+ </div>
133
+ {apiErrorMsg && <Alert className="mt-2" msg={apiErrorMsg} />}
134
+ {errorsMsg?.length > 0 &&
135
+ errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
136
+ </form>
137
+ </FormProvider>
138
+ );
139
+ };
140
+
141
+ export default EHFForm;
@@ -7,6 +7,7 @@ import { PaymentMethodEnum } from '../../enums/general';
7
7
  import { WRONG_MSG, PAYMENT_METHOD_DEFAULT } from '../../constants/index';
8
8
  import Alert from '../Alert/Alert';
9
9
  import Button from '../Button/Button';
10
+ import ToggleField from '../FormFields/ToggleFiled';
10
11
  import formFieldsMapper from '../FormFields/FormFieldsMapper';
11
12
  import { DEFAULT_ORDER_FORM_FIELDS } from '../FormFields/FormFields.helper';
12
13
  import { OrderFormInputsType, OrderInfoType } from '../../types/order';
@@ -16,8 +17,10 @@ import {
16
17
  ErrorsMsg,
17
18
  } from '../../types/general';
18
19
  import {
20
+ orderInvoiceContactFields,
19
21
  prepareAgreementModel,
20
22
  prepareContactModel,
23
+ prepareInvoiceModel,
21
24
  } from '../../utils/order.helper';
22
25
  import {
23
26
  prepareErrorMessage,
@@ -40,14 +43,20 @@ type Props = {
40
43
  paymentMethodsOptions?: PaymentMethodOptionsType;
41
44
  defaultValues?: OrderFormInputsType;
42
45
  paymentMethods?: { label: string; value: PaymentMethodEnum }[];
43
- buttonText?: string;
46
+ submitButtonText?: string;
44
47
  paymentMethodLabel?: string;
45
48
  errorReqMsg?: string;
46
49
  errorInvalidEmailMsg?: string;
47
50
  errorInvalidPhoneMsg?: string;
51
+ invoiceAddressSelection?: {
52
+ enabled?: boolean;
53
+ label?: string;
54
+ fields?: OrderFormFiledType[];
55
+ };
48
56
  };
49
57
 
50
58
  const initialData = {
59
+ invoiceAddressSelection: false,
51
60
  name: '',
52
61
  email: '',
53
62
  phoneNumber: '',
@@ -56,6 +65,7 @@ const initialData = {
56
65
  address: '',
57
66
  zip: '',
58
67
  paymentMethod: PAYMENT_METHOD_DEFAULT,
68
+ orderReference: '',
59
69
  };
60
70
 
61
71
  const OrderForm: FC<Props> = ({
@@ -72,7 +82,8 @@ const OrderForm: FC<Props> = ({
72
82
  language,
73
83
  merchantAgreementUrl,
74
84
  paymentMethods = [],
75
- buttonText = 'Start',
85
+ invoiceAddressSelection,
86
+ submitButtonText = 'Start',
76
87
  paymentMethodLabel = 'Select Payment Method',
77
88
  errorReqMsg = '',
78
89
  errorInvalidEmailMsg = '',
@@ -95,14 +106,19 @@ const OrderForm: FC<Props> = ({
95
106
  defaultValues?.paymentMethod || PAYMENT_METHOD_DEFAULT,
96
107
  },
97
108
  });
109
+ const invoiceOrderFields =
110
+ invoiceAddressSelection?.fields || orderInvoiceContactFields;
98
111
 
99
112
  const {
100
113
  register,
101
114
  handleSubmit,
102
115
  setValue,
116
+ watch,
103
117
  formState: { errors },
104
118
  } = methods;
105
119
 
120
+ const invoiceAddressToggle = watch('invoiceAddressSelection');
121
+
106
122
  const onSubmit: SubmitHandler<OrderFormInputsType> = async (
107
123
  data
108
124
  ): Promise<void> => {
@@ -158,6 +174,13 @@ const OrderForm: FC<Props> = ({
158
174
  templateSubscriptionPlanId: templatePackageId,
159
175
  },
160
176
  paymentAgreement: { ...agreements },
177
+ invoiceContact: invoiceAddressToggle
178
+ ? prepareInvoiceModel({
179
+ data,
180
+ orderFields: invoiceOrderFields,
181
+ })
182
+ : undefined,
183
+ orderReference: data.orderReference || undefined,
161
184
  });
162
185
  callback(responseOrder.terminalRedirectUrl, {
163
186
  orderId: responseOrder.id,
@@ -256,10 +279,39 @@ const OrderForm: FC<Props> = ({
256
279
  })}
257
280
  </Suspense>
258
281
  )}
282
+ {invoiceAddressSelection?.enabled && (
283
+ <ToggleField
284
+ name="invoiceAddressSelection"
285
+ label={
286
+ invoiceAddressSelection?.label ?? 'Invoice Address'
287
+ }
288
+ />
289
+ )}
290
+ {invoiceAddressToggle && invoiceOrderFields?.length > 0 && (
291
+ <Suspense fallback={null}>
292
+ {invoiceOrderFields.map((field) => {
293
+ const Component = formFieldsMapper[field.name];
294
+
295
+ return Component ? (
296
+ <Component
297
+ key={field.name}
298
+ name={field.name}
299
+ label={field.label}
300
+ required={field.required || false}
301
+ readOnly={field.readOnly || false}
302
+ errors={errors}
303
+ errorReqMsg={errorReqMsg}
304
+ errorInvalidEmailMsg={errorInvalidEmailMsg}
305
+ errorInvalidPhoneMsg={errorInvalidPhoneMsg}
306
+ />
307
+ ) : null;
308
+ })}
309
+ </Suspense>
310
+ )}
259
311
  <Button
260
312
  type="submit"
261
313
  loading={loading}
262
- buttonText={buttonText}
314
+ buttonText={submitButtonText}
263
315
  />
264
316
  <Alert className="mt-2" msg={apiErrorMsg} />
265
317
  {errorsMsg?.length > 0 &&
@@ -16,3 +16,15 @@ export enum MessageEventTypeEnum {
16
16
  CANCEL = 'cancel',
17
17
  COMPLETE = 'complete',
18
18
  }
19
+
20
+ export enum InvoiceLookupNetworkEnum {
21
+ EHF = '0192',
22
+ OIO_GLN = '0088',
23
+ OIO_Danish_CVR = '0096',
24
+ }
25
+
26
+ export enum FormTypeEnum {
27
+ ORDER = 'order',
28
+ EHF = 'EHF',
29
+ OIO = 'OIO',
30
+ }
package/src/types/api.ts CHANGED
@@ -20,4 +20,5 @@ export type CreateSubscriberRequestType = {
20
20
  addressLines?: string[];
21
21
  zip?: string;
22
22
  careOf?: string;
23
+ buyerReference?: string;
23
24
  };
@@ -38,10 +38,19 @@ export type ConfigType = {
38
38
  paymentMethodsOptions?: PaymentMethodOptionsType;
39
39
  language?: string;
40
40
  merchantAgreementUrl?: string;
41
+ invoiceAddressSelection?: {
42
+ enabled?: boolean;
43
+ label?: string;
44
+ fields?: OrderFormFiledType[];
45
+ };
41
46
  settings?: {
42
47
  successText?: string;
43
48
  failureText?: string;
44
- buttonText?: string;
49
+ submitButtonText?: string;
50
+ backButtonText?: string;
51
+ verifyButtonText?: string;
52
+ organizationNumberLabel?: string;
53
+ glnLabel?: string;
45
54
  orderDefaultValues?: OrderFormInputsType;
46
55
  paymentMethodLabel?: string;
47
56
  errorReqMsg?: string;
@@ -10,6 +10,17 @@ export type OrderFormInputsType = {
10
10
  address?: string;
11
11
  zip?: string;
12
12
  careOf?: string;
13
+ invoiceAddressSelection?: boolean;
14
+ orderReference?: string;
15
+ invoiceName?: string;
16
+ invoiceEmail?: string;
17
+ invoicePhoneNumber?: string;
18
+ invoiceCountry?: string;
19
+ invoiceCity?: string;
20
+ invoiceAddress?: string;
21
+ invoiceZip?: string;
22
+ invoiceCareOf?: string;
23
+ invoiceBuyerReference?: string;
13
24
  };
14
25
 
15
26
  export type VippsMobilePay = {
@@ -1,6 +1,7 @@
1
1
  import { MessageEventTypeEnum, PaymentMethodEnum } from '../enums/general';
2
2
  import { AgreementsType, OrderFormInputsType } from '../types/order';
3
3
  import { OrderFormFiledType } from '../types/general';
4
+ import { FIELD_TYPES } from '../components/FormFields/FormFields.helper';
4
5
 
5
6
  type AgreementModelType = {
6
7
  paymentMethod: PaymentMethodEnum;
@@ -112,9 +113,111 @@ export const prepareContactModel = ({
112
113
  ? data.zip?.trim() || undefined
113
114
  : undefined,
114
115
  careOf: orderFields.find((i) => i.name === 'careOf')
115
- ? data.zip?.trim() || undefined
116
+ ? data.careOf?.trim() || undefined
116
117
  : undefined,
117
118
  };
118
119
 
119
120
  return model;
120
121
  };
122
+
123
+ export const prepareInvoiceModel = ({
124
+ data,
125
+ orderFields,
126
+ }: ContactModelType) => {
127
+ const model = {
128
+ name: orderFields.find((i) => i.name === 'invoiceName')
129
+ ? data.invoiceName?.trim() || undefined
130
+ : undefined,
131
+ email: orderFields.find((i) => i.name === 'invoiceEmail')
132
+ ? data.invoiceEmail?.trim() || undefined
133
+ : undefined,
134
+ phone: orderFields.find((i) => i.name === 'invoicePhoneNumber')
135
+ ? data.invoicePhoneNumber?.trim() || undefined
136
+ : undefined,
137
+ country: orderFields.find((i) => i.name === 'invoiceCountry')
138
+ ? data.invoiceCountry?.trim() || undefined
139
+ : undefined,
140
+ city: orderFields.find((i) => i.name === 'invoiceCity')
141
+ ? data.invoiceCity?.trim() || undefined
142
+ : undefined,
143
+ addressLines: orderFields.find((i) => i.name === 'invoiceAddress')
144
+ ? data.invoiceAddress?.trim()
145
+ ? data.invoiceAddress
146
+ ?.trim()
147
+ .replace(/\r\n/g, '\n')
148
+ .split('\n')
149
+ .filter((line) => line)
150
+ : undefined
151
+ : undefined,
152
+ zip: orderFields.find((i) => i.name === 'invoiceZip')
153
+ ? data.invoiceZip?.trim() || undefined
154
+ : undefined,
155
+ careOf: orderFields.find((i) => i.name === 'invoiceCareOf')
156
+ ? data.invoiceCareOf?.trim() || undefined
157
+ : undefined,
158
+ buyerReference: orderFields.find(
159
+ (i) => i.name === 'invoiceBuyerReference'
160
+ )
161
+ ? data.invoiceBuyerReference?.trim() || undefined
162
+ : undefined,
163
+ };
164
+
165
+ return model;
166
+ };
167
+
168
+ export const orderInvoiceContactFields = [
169
+ {
170
+ name: FIELD_TYPES.INVOICE_NAME,
171
+ label: 'Name',
172
+ required: false,
173
+ readOnly: false,
174
+ },
175
+ {
176
+ name: FIELD_TYPES.INVOICE_EMAIL,
177
+ label: 'Email',
178
+ required: false,
179
+ readOnly: false,
180
+ },
181
+ {
182
+ name: FIELD_TYPES.INVOICE_PHONE,
183
+ label: 'Phone',
184
+ required: false,
185
+ readOnly: false,
186
+ },
187
+ {
188
+ name: FIELD_TYPES.INVOICE_COUNTRY,
189
+ label: 'Country',
190
+ required: false,
191
+ readOnly: false,
192
+ },
193
+ {
194
+ name: FIELD_TYPES.INVOICE_CITY,
195
+ label: 'City',
196
+ required: false,
197
+ readOnly: false,
198
+ },
199
+ {
200
+ name: FIELD_TYPES.INVOICE_ADDRESS,
201
+ label: 'Address',
202
+ required: false,
203
+ readOnly: false,
204
+ },
205
+ {
206
+ name: FIELD_TYPES.INVOICE_ZIP,
207
+ label: 'Zip',
208
+ required: false,
209
+ readOnly: false,
210
+ },
211
+ {
212
+ name: FIELD_TYPES.INVOICE_CARE_OF,
213
+ label: 'Contact Person',
214
+ required: false,
215
+ readOnly: false,
216
+ },
217
+ {
218
+ name: FIELD_TYPES.INVOICE_BUYER_REFERENCE,
219
+ label: 'Buyer Reference',
220
+ required: false,
221
+ readOnly: false,
222
+ },
223
+ ];