@infosoftas/s4-orders-js-sdk 0.1.13-0 → 0.1.13

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.1.13-0",
3
+ "version": "0.1.13",
4
4
  "scripts": {
5
5
  "build": "rimraf lib && webpack --mode production",
6
6
  "release": "release-it"
@@ -27,13 +27,13 @@
27
27
  "copy-webpack-plugin": "^14.0.0",
28
28
  "css-loader": "^7.1.2",
29
29
  "mini-css-extract-plugin": "^2.9.4",
30
- "release-it": "^19.0.6",
30
+ "release-it": "^20.0.0",
31
31
  "rimraf": "^6.1.2",
32
32
  "sass": "^1.94.2",
33
33
  "sass-loader": "^16.0.6",
34
34
  "style-loader": "^4.0.0",
35
35
  "ts-loader": "^9.5.4",
36
- "typescript": "^5.9.3",
36
+ "typescript": "^6.0.2",
37
37
  "webpack": "^5.105.2",
38
38
  "webpack-cli": "^6.0.1"
39
39
  },
package/src/App.tsx CHANGED
@@ -68,6 +68,13 @@ const App: FC<ConfigType> = ({
68
68
  'The order/subscription will not be created because the subscriber has a denial order blocking all.',
69
69
  errorValidationBlockingOffersMsg:
70
70
  'The order/subscription will not be created because the subscriber has a denial order blocking offers.',
71
+ orderDenialOfferBaseText:
72
+ 'Hello again!\n\nLooks like you already tried this offer.',
73
+ orderDenialOfferWithFallbackText:
74
+ 'Hello again!\n\nLooks like you already tried this offer, but we would like to keep you with us.\n\nTherefore we got a new great offer for you:',
75
+ orderDenialAmountText:
76
+ 'We are sorry, but you do not meet the requirements for this offer. Please contact customer service for more information.',
77
+ orderDenialFallbackOffer: undefined,
71
78
  paymentMethodNotAllowedMsg: '',
72
79
  invoiceLookupNotFoundText:
73
80
  'There was no recipient found for the given information',
@@ -344,6 +351,12 @@ const App: FC<ConfigType> = ({
344
351
  callback={handleInvoiceForm}
345
352
  backButtonText={settings?.backButtonText}
346
353
  verifyButtonText={settings?.verifyButtonText}
354
+ orderDenialCloseButtonText={
355
+ settings?.orderDenialCloseButtonText
356
+ }
357
+ orderDenialContinueButtonText={
358
+ settings?.orderDenialContinueButtonText
359
+ }
347
360
  organizationNumberLabel={settings?.cvrLabel}
348
361
  glnLabel={settings?.glnLabel}
349
362
  organizationNumber={
@@ -377,6 +390,18 @@ const App: FC<ConfigType> = ({
377
390
  errorValidationBlockingOffersMsg={
378
391
  settings?.errorValidationBlockingOffersMsg
379
392
  }
393
+ orderDenialOfferBaseText={
394
+ settings?.orderDenialOfferBaseText
395
+ }
396
+ orderDenialOfferWithFallbackText={
397
+ settings?.orderDenialOfferWithFallbackText
398
+ }
399
+ orderDenialAmountText={
400
+ settings?.orderDenialAmountText
401
+ }
402
+ orderDenialFallbackOffer={
403
+ settings?.orderDenialFallbackOffer
404
+ }
380
405
  />
381
406
  )}
382
407
  {formType === FormTypeEnum.EHF && (
@@ -385,6 +410,12 @@ const App: FC<ConfigType> = ({
385
410
  callback={handleInvoiceForm}
386
411
  backButtonText={settings?.backButtonText}
387
412
  verifyButtonText={settings?.verifyButtonText}
413
+ orderDenialCloseButtonText={
414
+ settings?.orderDenialCloseButtonText
415
+ }
416
+ orderDenialContinueButtonText={
417
+ settings?.orderDenialContinueButtonText
418
+ }
388
419
  organizationNumberLabel={
389
420
  settings?.organizationNumberLabel
390
421
  }
@@ -419,6 +450,18 @@ const App: FC<ConfigType> = ({
419
450
  errorValidationBlockingOffersMsg={
420
451
  settings?.errorValidationBlockingOffersMsg
421
452
  }
453
+ orderDenialOfferBaseText={
454
+ settings?.orderDenialOfferBaseText
455
+ }
456
+ orderDenialOfferWithFallbackText={
457
+ settings?.orderDenialOfferWithFallbackText
458
+ }
459
+ orderDenialAmountText={
460
+ settings?.orderDenialAmountText
461
+ }
462
+ orderDenialFallbackOffer={
463
+ settings?.orderDenialFallbackOffer
464
+ }
422
465
  />
423
466
  )}
424
467
  {formType !== FormTypeEnum.OIO &&
@@ -440,6 +483,12 @@ const App: FC<ConfigType> = ({
440
483
  submitButtonText={
441
484
  settings?.submitButtonText
442
485
  }
486
+ orderDenialCloseButtonText={
487
+ settings?.orderDenialCloseButtonText
488
+ }
489
+ orderDenialContinueButtonText={
490
+ settings?.orderDenialContinueButtonText
491
+ }
443
492
  paymentMethodLabel={
444
493
  settings?.paymentMethodLabel
445
494
  }
@@ -475,6 +524,18 @@ const App: FC<ConfigType> = ({
475
524
  errorValidationBlockingOffersMsg={
476
525
  settings?.errorValidationBlockingOffersMsg
477
526
  }
527
+ orderDenialOfferBaseText={
528
+ settings?.orderDenialOfferBaseText
529
+ }
530
+ orderDenialOfferWithFallbackText={
531
+ settings?.orderDenialOfferWithFallbackText
532
+ }
533
+ orderDenialAmountText={
534
+ settings?.orderDenialAmountText
535
+ }
536
+ orderDenialFallbackOffer={
537
+ settings?.orderDenialFallbackOffer
538
+ }
478
539
  termsAndConditionsText={
479
540
  settings?.termsAndConditionsText
480
541
  }
@@ -497,7 +558,9 @@ const App: FC<ConfigType> = ({
497
558
  {isFailed && <Alert className="mt-2" msg={failedMsg} />}
498
559
  {isFailed &&
499
560
  errorsMsg?.length > 0 &&
500
- errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
561
+ errorsMsg.map((i, index) => (
562
+ <Alert key={`${i}-${index}`} className="mt-2" msg={i} />
563
+ ))}
501
564
  </div>
502
565
  </ErrorBoundary>
503
566
  );
@@ -3,12 +3,14 @@ import { FormProvider, useForm, SubmitHandler } from 'react-hook-form';
3
3
 
4
4
  import Alert from '../Alert/Alert';
5
5
  import Button from '../Button/Button';
6
+ import OrderDenialModal from '../OrderDenialModal/OrderDenialModal';
6
7
  import InputField from '../FormFields/InputField';
7
8
  import { PaymentMethodEnum, UserActionEnum } from '../../enums/general';
8
9
  import {
9
10
  OrderFormFieldType,
10
11
  PaymentMethodOptionsType,
11
12
  ContactRequestType,
13
+ OrderDenialFallbackOfferType,
12
14
  } from '../../types/general';
13
15
  import { OrderFormInputsType, OrderInfoType } from '../../types/order';
14
16
  import { orderInvoiceContactFields } from '../../utils/order.helper';
@@ -27,6 +29,8 @@ type Props = {
27
29
  className?: string;
28
30
  backButtonText?: string;
29
31
  verifyButtonText?: string;
32
+ orderDenialCloseButtonText?: string;
33
+ orderDenialContinueButtonText?: string;
30
34
  organizationNumberLabel?: string;
31
35
  organizationNumber?: string;
32
36
  glnLabel?: string;
@@ -51,6 +55,10 @@ type Props = {
51
55
  errorValidationTitleMsg?: string;
52
56
  errorValidationDenialOrderBlockingMsg?: string;
53
57
  errorValidationBlockingOffersMsg?: string;
58
+ orderDenialOfferBaseText?: string;
59
+ orderDenialOfferWithFallbackText?: string;
60
+ orderDenialAmountText?: string;
61
+ orderDenialFallbackOffer?: OrderDenialFallbackOfferType;
54
62
  };
55
63
 
56
64
  type EHFFormInputsType = {
@@ -70,6 +78,8 @@ const EHFForm: FC<Props> = ({
70
78
  className = '',
71
79
  backButtonText = '',
72
80
  verifyButtonText = '',
81
+ orderDenialCloseButtonText = 'Close',
82
+ orderDenialContinueButtonText = 'Continue',
73
83
  organizationNumberLabel = '',
74
84
  organizationNumber = '',
75
85
  templatePackageId,
@@ -88,6 +98,10 @@ const EHFForm: FC<Props> = ({
88
98
  errorValidationTitleMsg,
89
99
  errorValidationDenialOrderBlockingMsg,
90
100
  errorValidationBlockingOffersMsg,
101
+ orderDenialOfferBaseText,
102
+ orderDenialOfferWithFallbackText,
103
+ orderDenialAmountText,
104
+ orderDenialFallbackOffer,
91
105
  }) => {
92
106
  const methods = useForm<EHFFormInputsType>({
93
107
  defaultValues: {
@@ -108,7 +122,16 @@ const EHFForm: FC<Props> = ({
108
122
  paymentMethodsOptions?.[orderValues?.paymentMethod as PaymentMethodEnum]
109
123
  ?.orderFormFields ?? DEFAULT_ORDER_FORM_FIELDS;
110
124
 
111
- const { orderSubmit, loading, apiErrorMsg, errorsMsg } = useOrderForm({
125
+ const {
126
+ orderSubmit,
127
+ continueWithFallbackOffer,
128
+ loading,
129
+ apiErrorMsg,
130
+ errorsMsg,
131
+ orderDenialType,
132
+ orderDenialMessage,
133
+ dismissOrderDenial,
134
+ } = useOrderForm({
112
135
  callback,
113
136
  submitStartCallback,
114
137
  userActionCallback,
@@ -130,6 +153,10 @@ const EHFForm: FC<Props> = ({
130
153
  errorValidationTitleMsg,
131
154
  errorValidationDenialOrderBlockingMsg,
132
155
  errorValidationBlockingOffersMsg,
156
+ orderDenialOfferBaseText,
157
+ orderDenialOfferWithFallbackText,
158
+ orderDenialAmountText,
159
+ orderDenialFallbackOffer,
133
160
  });
134
161
 
135
162
  const onSubmit: SubmitHandler<EHFFormInputsType> = async (
@@ -178,7 +205,27 @@ const EHFForm: FC<Props> = ({
178
205
 
179
206
  {apiErrorMsg && <Alert className="mt-2" msg={apiErrorMsg} />}
180
207
  {errorsMsg?.length > 0 &&
181
- errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
208
+ errorsMsg.map((i, index) => (
209
+ <Alert key={index} className="mt-2" msg={i} />
210
+ ))}
211
+ <OrderDenialModal
212
+ isOpen={!!orderDenialType}
213
+ message={orderDenialMessage}
214
+ closeButtonText={orderDenialCloseButtonText}
215
+ continueButtonText={orderDenialContinueButtonText}
216
+ canContinue={
217
+ orderDenialType === 'offer' &&
218
+ !!orderDenialFallbackOffer?.templatePackageId
219
+ }
220
+ offer={
221
+ orderDenialType === 'offer' &&
222
+ !!orderDenialFallbackOffer?.templatePackageId
223
+ ? orderDenialFallbackOffer
224
+ : undefined
225
+ }
226
+ onClose={dismissOrderDenial}
227
+ onContinue={continueWithFallbackOffer}
228
+ />
182
229
  </form>
183
230
  </FormProvider>
184
231
  );
@@ -3,6 +3,7 @@ import { FormProvider, useForm, SubmitHandler } from 'react-hook-form';
3
3
 
4
4
  import Alert from '../Alert/Alert';
5
5
  import Button from '../Button/Button';
6
+ import OrderDenialModal from '../OrderDenialModal/OrderDenialModal';
6
7
  import InputField from '../FormFields/InputField';
7
8
  import { DEFAULT_ORDER_FORM_FIELDS } from '../FormFields/FormFields.helper';
8
9
 
@@ -11,6 +12,7 @@ import {
11
12
  OrderFormFieldType,
12
13
  PaymentMethodOptionsType,
13
14
  ContactRequestType,
15
+ OrderDenialFallbackOfferType,
14
16
  } from '../../types/general';
15
17
  import useOrderForm from '../../hooks/useOrderForm';
16
18
  import { orderInvoiceContactFields } from '../../utils/order.helper';
@@ -28,6 +30,8 @@ type Props = {
28
30
  className?: string;
29
31
  backButtonText?: string;
30
32
  verifyButtonText?: string;
33
+ orderDenialCloseButtonText?: string;
34
+ orderDenialContinueButtonText?: string;
31
35
  organizationNumberLabel?: string;
32
36
  organizationNumber?: string;
33
37
  glnLabel?: string;
@@ -52,6 +56,10 @@ type Props = {
52
56
  errorValidationTitleMsg?: string;
53
57
  errorValidationDenialOrderBlockingMsg?: string;
54
58
  errorValidationBlockingOffersMsg?: string;
59
+ orderDenialOfferBaseText?: string;
60
+ orderDenialOfferWithFallbackText?: string;
61
+ orderDenialAmountText?: string;
62
+ orderDenialFallbackOffer?: OrderDenialFallbackOfferType;
55
63
  };
56
64
 
57
65
  type OIOFormInputsType = {
@@ -64,7 +72,7 @@ const initialData = {
64
72
  gln: '',
65
73
  };
66
74
 
67
- const EHFForm: FC<Props> = ({
75
+ const OIOForm: FC<Props> = ({
68
76
  callback,
69
77
  onBack,
70
78
  submitStartCallback,
@@ -73,6 +81,8 @@ const EHFForm: FC<Props> = ({
73
81
  className = '',
74
82
  backButtonText = '',
75
83
  verifyButtonText = '',
84
+ orderDenialCloseButtonText = 'Close',
85
+ orderDenialContinueButtonText = 'Continue',
76
86
  organizationNumberLabel = '',
77
87
  organizationNumber = '',
78
88
  glnLabel = '',
@@ -92,6 +102,10 @@ const EHFForm: FC<Props> = ({
92
102
  errorValidationTitleMsg,
93
103
  errorValidationDenialOrderBlockingMsg,
94
104
  errorValidationBlockingOffersMsg,
105
+ orderDenialOfferBaseText,
106
+ orderDenialOfferWithFallbackText,
107
+ orderDenialAmountText,
108
+ orderDenialFallbackOffer,
95
109
  }) => {
96
110
  const methods = useForm<OIOFormInputsType>({
97
111
  defaultValues: {
@@ -116,7 +130,16 @@ const EHFForm: FC<Props> = ({
116
130
  paymentMethodsOptions?.[orderValues?.paymentMethod as PaymentMethodEnum]
117
131
  ?.orderFormFields ?? DEFAULT_ORDER_FORM_FIELDS;
118
132
 
119
- const { orderSubmit, loading, apiErrorMsg, errorsMsg } = useOrderForm({
133
+ const {
134
+ orderSubmit,
135
+ continueWithFallbackOffer,
136
+ loading,
137
+ apiErrorMsg,
138
+ errorsMsg,
139
+ orderDenialType,
140
+ orderDenialMessage,
141
+ dismissOrderDenial,
142
+ } = useOrderForm({
120
143
  callback,
121
144
  submitStartCallback,
122
145
  userActionCallback,
@@ -138,6 +161,10 @@ const EHFForm: FC<Props> = ({
138
161
  errorValidationTitleMsg,
139
162
  errorValidationDenialOrderBlockingMsg,
140
163
  errorValidationBlockingOffersMsg,
164
+ orderDenialOfferBaseText,
165
+ orderDenialOfferWithFallbackText,
166
+ orderDenialAmountText,
167
+ orderDenialFallbackOffer,
141
168
  });
142
169
 
143
170
  const onSubmit: SubmitHandler<OIOFormInputsType> = async (
@@ -189,10 +216,30 @@ const EHFForm: FC<Props> = ({
189
216
  </div>
190
217
  {apiErrorMsg && <Alert className="mt-2" msg={apiErrorMsg} />}
191
218
  {errorsMsg?.length > 0 &&
192
- errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
219
+ errorsMsg.map((i, index) => (
220
+ <Alert key={`${i}-${index}`} className="mt-2" msg={i} />
221
+ ))}
222
+ <OrderDenialModal
223
+ isOpen={!!orderDenialType}
224
+ message={orderDenialMessage}
225
+ closeButtonText={orderDenialCloseButtonText}
226
+ continueButtonText={orderDenialContinueButtonText}
227
+ canContinue={
228
+ orderDenialType === 'offer' &&
229
+ !!orderDenialFallbackOffer?.templatePackageId
230
+ }
231
+ offer={
232
+ orderDenialType === 'offer' &&
233
+ !!orderDenialFallbackOffer?.templatePackageId
234
+ ? orderDenialFallbackOffer
235
+ : undefined
236
+ }
237
+ onClose={dismissOrderDenial}
238
+ onContinue={continueWithFallbackOffer}
239
+ />
193
240
  </form>
194
241
  </FormProvider>
195
242
  );
196
243
  };
197
244
 
198
- export default EHFForm;
245
+ export default OIOForm;
@@ -0,0 +1,91 @@
1
+ import { FC } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ import { OrderDenialFallbackOfferType } from '../../types/general';
5
+ import Button from '../Button/Button';
6
+
7
+ import './orderDenialModal.scss';
8
+
9
+ type Props = {
10
+ isOpen: boolean;
11
+ message?: string;
12
+ offer?: OrderDenialFallbackOfferType;
13
+ canContinue?: boolean;
14
+ closeButtonText?: string;
15
+ continueButtonText?: string;
16
+ onContinue: () => void;
17
+ onClose: () => void;
18
+ };
19
+
20
+ const OrderDenialModal: FC<Props> = ({
21
+ isOpen,
22
+ message,
23
+ offer,
24
+ canContinue = true,
25
+ closeButtonText = 'Close',
26
+ continueButtonText = 'Continue',
27
+ onContinue,
28
+ onClose,
29
+ }) => {
30
+ if (!isOpen || typeof document === 'undefined') {
31
+ return null;
32
+ }
33
+
34
+ return createPortal(
35
+ <div
36
+ className="sdk-order-denial-modal-overlay"
37
+ data-testid="sdk-order-denial-modal-overlay"
38
+ >
39
+ <div
40
+ className="sdk-order-denial-modal"
41
+ role="dialog"
42
+ aria-modal="true"
43
+ aria-label="Order denial modal"
44
+ >
45
+ <p className="sdk-order-denial-modal-text">
46
+ {message?.split('\\n').map((line, i, arr) => (
47
+ <span key={i}>
48
+ {line}
49
+ {i < arr.length - 1 && <br />}
50
+ </span>
51
+ ))}
52
+ </p>
53
+ {offer?.title && offer?.templatePackageId && (
54
+ <div className="sdk-order-denial-offer-box">
55
+ <h4 className="sdk-order-denial-offer-title">
56
+ {offer.title}
57
+ </h4>
58
+ {offer.description && (
59
+ <p className="sdk-order-denial-offer-description">
60
+ {offer.description}
61
+ </p>
62
+ )}
63
+ {offer.price && (
64
+ <p className="sdk-order-denial-offer-meta">
65
+ {offer.price}
66
+ </p>
67
+ )}
68
+ </div>
69
+ )}
70
+ <div className="sdk-order-denial-modal-actions">
71
+ <Button
72
+ type="button"
73
+ btnType="default"
74
+ buttonText={closeButtonText}
75
+ onClick={onClose}
76
+ />
77
+ {canContinue && (
78
+ <Button
79
+ type="button"
80
+ buttonText={continueButtonText}
81
+ onClick={onContinue}
82
+ />
83
+ )}
84
+ </div>
85
+ </div>
86
+ </div>,
87
+ document.body
88
+ );
89
+ };
90
+
91
+ export default OrderDenialModal;
@@ -0,0 +1,42 @@
1
+ .sdk-order-denial-modal-overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ z-index: 9999;
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ padding: 16px;
9
+ }
10
+
11
+ .sdk-order-denial-modal {
12
+ width: min(560px, 100%);
13
+ padding: 24px;
14
+ }
15
+
16
+ .sdk-order-denial-modal-text {
17
+ margin: 0;
18
+ }
19
+
20
+ .sdk-order-denial-offer-box {
21
+ margin-top: 16px;
22
+ padding: 14px;
23
+ }
24
+
25
+ .sdk-order-denial-offer-title {
26
+ margin: 0;
27
+ }
28
+
29
+ .sdk-order-denial-offer-description {
30
+ margin: 8px 0 0;
31
+ }
32
+
33
+ .sdk-order-denial-offer-meta {
34
+ margin: 8px 0 0;
35
+ }
36
+
37
+ .sdk-order-denial-modal-actions {
38
+ margin-top: 20px;
39
+ display: flex;
40
+ justify-content: flex-end;
41
+ gap: 10px;
42
+ }
@@ -1,4 +1,12 @@
1
- import { FC, ChangeEvent, Suspense, useState, useMemo, useEffect } from 'react';
1
+ import {
2
+ FC,
3
+ ChangeEvent,
4
+ ReactNode,
5
+ Suspense,
6
+ useState,
7
+ useMemo,
8
+ useEffect,
9
+ } from 'react';
2
10
  import { FormProvider, useForm, SubmitHandler } from 'react-hook-form';
3
11
 
4
12
  import { PaymentMethodEnum, UserActionEnum } from '../../enums/general';
@@ -8,6 +16,7 @@ import {
8
16
  } from '../../constants/index';
9
17
  import Alert from '../Alert/Alert';
10
18
  import Button from '../Button/Button';
19
+ import OrderDenialModal from '../OrderDenialModal/OrderDenialModal';
11
20
  import ToggleField from '../FormFields/ToggleFiled';
12
21
  import formFieldsMapper from '../FormFields/FormFieldsMapper';
13
22
  import { DEFAULT_ORDER_FORM_FIELDS } from '../FormFields/FormFields.helper';
@@ -16,6 +25,7 @@ import {
16
25
  PaymentMethodOptionsType,
17
26
  OrderFormFieldType,
18
27
  ContactRequestType,
28
+ OrderDenialFallbackOfferType,
19
29
  } from '../../types/general';
20
30
  import { orderInvoiceContactFields } from '../../utils/order.helper';
21
31
  import useOrderForm from '../../hooks/useOrderForm';
@@ -46,6 +56,8 @@ type Props = {
46
56
  paymentMethods?: { label: string; value: PaymentMethodEnum }[];
47
57
  allowedPaymentMethods?: PaymentMethodEnum[];
48
58
  submitButtonText?: string;
59
+ orderDenialCloseButtonText?: string;
60
+ orderDenialContinueButtonText?: string;
49
61
  paymentMethodLabel?: string;
50
62
  paymentMethodNotAllowedMsg?: string;
51
63
  errorReqMsg?: string;
@@ -61,7 +73,11 @@ type Props = {
61
73
  errorValidationTitleMsg?: string;
62
74
  errorValidationDenialOrderBlockingMsg?: string;
63
75
  errorValidationBlockingOffersMsg?: string;
64
- termsAndConditionsText?: string;
76
+ orderDenialOfferBaseText?: string;
77
+ orderDenialOfferWithFallbackText?: string;
78
+ orderDenialAmountText?: string;
79
+ orderDenialFallbackOffer?: OrderDenialFallbackOfferType;
80
+ termsAndConditionsText?: string | ReactNode;
65
81
  };
66
82
 
67
83
  const initialData = {
@@ -98,6 +114,8 @@ const OrderForm: FC<Props> = ({
98
114
  allowedPaymentMethods,
99
115
  invoiceAddressSelection,
100
116
  submitButtonText = 'Start',
117
+ orderDenialCloseButtonText = 'Close',
118
+ orderDenialContinueButtonText = 'Continue',
101
119
  paymentMethodLabel = 'Select Payment Method',
102
120
  paymentMethodNotAllowedMsg = 'This payment method not allowed!',
103
121
  errorReqMsg = '',
@@ -106,6 +124,10 @@ const OrderForm: FC<Props> = ({
106
124
  errorValidationTitleMsg,
107
125
  errorValidationDenialOrderBlockingMsg,
108
126
  errorValidationBlockingOffersMsg,
127
+ orderDenialOfferBaseText,
128
+ orderDenialOfferWithFallbackText,
129
+ orderDenialAmountText,
130
+ orderDenialFallbackOffer,
109
131
  requireTermsAcceptance,
110
132
  termsAndConditionsText,
111
133
  }) => {
@@ -175,7 +197,16 @@ const OrderForm: FC<Props> = ({
175
197
  invoiceAddressSelection?.paymentMethods ||
176
198
  INVOICE_ALLOWED_PAYMENT_METHODS;
177
199
 
178
- const { orderSubmit, loading, apiErrorMsg, errorsMsg } = useOrderForm({
200
+ const {
201
+ orderSubmit,
202
+ continueWithFallbackOffer,
203
+ loading,
204
+ apiErrorMsg,
205
+ errorsMsg,
206
+ orderDenialType,
207
+ orderDenialMessage,
208
+ dismissOrderDenial,
209
+ } = useOrderForm({
179
210
  callback,
180
211
  submitStartCallback,
181
212
  userActionCallback,
@@ -196,6 +227,10 @@ const OrderForm: FC<Props> = ({
196
227
  errorValidationTitleMsg,
197
228
  errorValidationDenialOrderBlockingMsg,
198
229
  errorValidationBlockingOffersMsg,
230
+ orderDenialOfferBaseText,
231
+ orderDenialOfferWithFallbackText,
232
+ orderDenialAmountText,
233
+ orderDenialFallbackOffer,
199
234
  });
200
235
 
201
236
  const onSubmit: SubmitHandler<OrderFormInputsType> = async (
@@ -380,7 +415,27 @@ const OrderForm: FC<Props> = ({
380
415
  />
381
416
  {errorsMsg?.length > 0 &&
382
417
  allowPaymentMethod &&
383
- errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
418
+ errorsMsg.map((i, index) => (
419
+ <Alert key={`${i}-${index}`} className="mt-2" msg={i} />
420
+ ))}
421
+ <OrderDenialModal
422
+ isOpen={!!orderDenialType}
423
+ message={orderDenialMessage}
424
+ closeButtonText={orderDenialCloseButtonText}
425
+ continueButtonText={orderDenialContinueButtonText}
426
+ canContinue={
427
+ orderDenialType === 'offer' &&
428
+ !!orderDenialFallbackOffer?.templatePackageId
429
+ }
430
+ offer={
431
+ orderDenialType === 'offer' &&
432
+ !!orderDenialFallbackOffer?.templatePackageId
433
+ ? orderDenialFallbackOffer
434
+ : undefined
435
+ }
436
+ onClose={dismissOrderDenial}
437
+ onContinue={continueWithFallbackOffer}
438
+ />
384
439
  </form>
385
440
  </FormProvider>
386
441
  );
@@ -0,0 +1,4 @@
1
+ declare module '*.scss' {
2
+ const content: { [className: string]: string };
3
+ export default content;
4
+ }
@@ -32,6 +32,7 @@ export enum FormTypeEnum {
32
32
  export enum UserActionEnum {
33
33
  SELECT_FORM = 'selectForm',
34
34
  SELECT_PAYMENT_METHOD = 'selectPaymentMethod',
35
+ SELECT_FALLBACK_OFFER = 'selectFallbackOffer',
35
36
  RETURN_TO_MAIN = 'returnToMain',
36
37
  TOGGLE_INVOICE_ADDRESS = 'toggleInvoiceAddress',
37
38
  SEARCH_ORGANIZATION_NUMBER = 'searchOrganizationNumber',
@@ -1,4 +1,4 @@
1
- import { useState } from 'react';
1
+ import { useRef, useState } from 'react';
2
2
  import { SubmitHandler } from 'react-hook-form';
3
3
 
4
4
  import { createSubscriber } from '../api/SubscribeApi';
@@ -14,6 +14,7 @@ import { OrderFormInputsType, OrderInfoType } from '../types/order';
14
14
  import {
15
15
  ErrorsMsg,
16
16
  OrderFormFieldType,
17
+ OrderDenialFallbackOfferType,
17
18
  PaymentMethodOptionsType,
18
19
  ContactRequestType,
19
20
  } from '../types/general';
@@ -35,9 +36,7 @@ type Props = {
35
36
  action: UserActionEnum,
36
37
  args: object | null | undefined
37
38
  ) => void;
38
- setContactCallback?: (
39
- contactInfo: ContactRequestType
40
- ) => void;
39
+ setContactCallback?: (contactInfo: ContactRequestType) => void;
41
40
  templatePackageId: string;
42
41
  subscriberId?: string;
43
42
  userId?: string;
@@ -55,6 +54,52 @@ type Props = {
55
54
  errorValidationTitleMsg?: string;
56
55
  errorValidationDenialOrderBlockingMsg?: string;
57
56
  errorValidationBlockingOffersMsg?: string;
57
+ orderDenialOfferBaseText?: string;
58
+ orderDenialOfferWithFallbackText?: string;
59
+ orderDenialAmountText?: string;
60
+ orderDenialFallbackOffer?: OrderDenialFallbackOfferType;
61
+ };
62
+
63
+ type DenialErrorType = {
64
+ status?: number;
65
+ errors?: Record<string, string[] | undefined>;
66
+ };
67
+
68
+ const OFFER_DENIAL_BASE_TEXT =
69
+ 'Hello again!\n\nLooks like you already tried this offer.';
70
+ const OFFER_DENIAL_WITH_FALLBACK_TEXT =
71
+ 'Hello again!\n\nLooks like you already tried this offer, but we would like to keep you with us.\n\nTherefore we got a new great offer for you:';
72
+
73
+ const getDenialErrorType = (
74
+ error: DenialErrorType
75
+ ): 'offer' | 'amount' | null => {
76
+ if (error?.status !== 400 || !error?.errors) {
77
+ return null;
78
+ }
79
+
80
+ const normalizedEntries = Object.entries(error.errors).map(
81
+ ([key, value]) => [key.toLowerCase(), value] as const
82
+ );
83
+
84
+ const hasOffer = normalizedEntries.some(
85
+ ([key, value]) =>
86
+ key === 'offer' && Array.isArray(value) && value.length > 0
87
+ );
88
+
89
+ if (hasOffer) {
90
+ return 'offer';
91
+ }
92
+
93
+ const hasAmount = normalizedEntries.some(
94
+ ([key, value]) =>
95
+ key === 'amount' && Array.isArray(value) && value.length > 0
96
+ );
97
+
98
+ if (hasAmount) {
99
+ return 'amount';
100
+ }
101
+
102
+ return null;
58
103
  };
59
104
 
60
105
  const useOrderForm = ({
@@ -79,17 +124,56 @@ const useOrderForm = ({
79
124
  errorValidationTitleMsg,
80
125
  errorValidationDenialOrderBlockingMsg,
81
126
  errorValidationBlockingOffersMsg,
127
+ orderDenialOfferBaseText,
128
+ orderDenialOfferWithFallbackText,
129
+ orderDenialAmountText,
130
+ orderDenialFallbackOffer,
82
131
  }: Props) => {
83
132
  const [loading, setLoading] = useState<boolean>(false);
84
133
  const [apiErrorMsg, setApiErrorMsg] = useState<string>('');
85
134
  const [errorsMsg, setErrorsMsg] = useState<string[]>([]);
135
+ const [orderDenialType, setOrderDenialType] = useState<
136
+ 'offer' | 'amount' | null
137
+ >(null);
138
+ const [submittedOrderData, setSubmittedOrderData] =
139
+ useState<OrderFormInputsType | null>(null);
140
+ const fallbackOfferTemplatePackageIdRef = useRef<string | null>(null);
141
+
142
+ const dismissOrderDenial = () => {
143
+ setOrderDenialType(null);
144
+ };
145
+
146
+ const getOrderDenialMessage = () => {
147
+ if (orderDenialType === 'offer') {
148
+ if (orderDenialFallbackOffer?.templatePackageId) {
149
+ return (
150
+ orderDenialOfferWithFallbackText ||
151
+ OFFER_DENIAL_WITH_FALLBACK_TEXT
152
+ );
153
+ }
154
+
155
+ return orderDenialOfferBaseText || OFFER_DENIAL_BASE_TEXT;
156
+ }
157
+
158
+ if (orderDenialType === 'amount') {
159
+ return (
160
+ orderDenialAmountText ||
161
+ errorValidationDenialOrderBlockingMsg ||
162
+ WRONG_MSG
163
+ );
164
+ }
165
+
166
+ return '';
167
+ };
86
168
 
87
169
  const orderSubmit: SubmitHandler<OrderFormInputsType> = async (
88
170
  data
89
171
  ): Promise<void> => {
90
172
  setApiErrorMsg('');
91
173
  setErrorsMsg([]);
174
+ setOrderDenialType(null);
92
175
  setLoading(true);
176
+ setSubmittedOrderData(data);
93
177
  try {
94
178
  let id: string | undefined = subscriberId;
95
179
  let invoiceResponse = null;
@@ -161,6 +245,10 @@ const useOrderForm = ({
161
245
  if (id) {
162
246
  const paymentMethod =
163
247
  data.paymentMethod || PAYMENT_METHOD_DEFAULT;
248
+ const selectedTemplateSubscriptionPlanId =
249
+ fallbackOfferTemplatePackageIdRef.current ||
250
+ templatePackageId;
251
+ fallbackOfferTemplatePackageIdRef.current = null;
164
252
 
165
253
  const agreements = prepareAgreementModel({
166
254
  paymentMethod,
@@ -186,7 +274,8 @@ const useOrderForm = ({
186
274
  subscriberId: id,
187
275
  subscriptionPlan: {
188
276
  organizationId,
189
- templateSubscriptionPlanId: templatePackageId,
277
+ templateSubscriptionPlanId:
278
+ selectedTemplateSubscriptionPlanId,
190
279
  },
191
280
  paymentAgreement: { ...agreements },
192
281
  invoiceContact: invoiceAddressToggle
@@ -216,6 +305,17 @@ const useOrderForm = ({
216
305
  errorValidationDenialOrderBlockingMsg,
217
306
  } as ErrorMessages;
218
307
 
308
+ const detectedDenialType = getDenialErrorType(
309
+ error as DenialErrorType
310
+ );
311
+ if (detectedDenialType) {
312
+ setApiErrorMsg('');
313
+ setErrorsMsg([]);
314
+ setOrderDenialType(detectedDenialType);
315
+ callback(null);
316
+ return;
317
+ }
318
+
219
319
  console.error(error);
220
320
  setApiErrorMsg(
221
321
  prepareErrorMessage(error as Error, undefined, translations)
@@ -236,11 +336,36 @@ const useOrderForm = ({
236
336
  }
237
337
  };
238
338
 
339
+ const continueWithFallbackOffer = async () => {
340
+ if (orderDenialType !== 'offer' || !submittedOrderData) {
341
+ return;
342
+ }
343
+
344
+ const fallbackTemplatePackageId =
345
+ orderDenialFallbackOffer?.templatePackageId;
346
+
347
+ if (!fallbackTemplatePackageId) {
348
+ dismissOrderDenial();
349
+ return;
350
+ }
351
+
352
+ fallbackOfferTemplatePackageIdRef.current = fallbackTemplatePackageId;
353
+ userActionCallback?.(UserActionEnum.SELECT_FALLBACK_OFFER, {
354
+ templatePackageId: fallbackTemplatePackageId,
355
+ });
356
+ dismissOrderDenial();
357
+ await orderSubmit(submittedOrderData);
358
+ };
359
+
239
360
  return {
240
361
  orderSubmit,
362
+ continueWithFallbackOffer,
241
363
  loading,
242
364
  apiErrorMsg,
243
365
  errorsMsg,
366
+ orderDenialType,
367
+ orderDenialMessage: getOrderDenialMessage(),
368
+ dismissOrderDenial,
244
369
  };
245
370
  };
246
371
 
@@ -1,3 +1,5 @@
1
+ import { ReactNode } from 'react';
2
+
1
3
  import { PaymentMethodEnum, UserActionEnum } from '../enums/general';
2
4
  import { OrderFormInputsType } from './order';
3
5
 
@@ -34,6 +36,13 @@ export type ContactRequestType = {
34
36
  careOf?: string;
35
37
  };
36
38
 
39
+ export type OrderDenialFallbackOfferType = {
40
+ title?: string;
41
+ description?: string;
42
+ price?: string;
43
+ templatePackageId?: string;
44
+ };
45
+
37
46
  export type ConfigType = {
38
47
  submitStartCallback?: (subscriberId: string) => void;
39
48
  userActionCallback?: (
@@ -70,6 +79,8 @@ export type ConfigType = {
70
79
  submitButtonText?: string;
71
80
  backButtonText?: string;
72
81
  verifyButtonText?: string;
82
+ orderDenialCloseButtonText?: string;
83
+ orderDenialContinueButtonText?: string;
73
84
  organizationNumberLabel?: string;
74
85
  cvrLabel?: string;
75
86
  glnLabel?: string;
@@ -83,7 +94,11 @@ export type ConfigType = {
83
94
  errorValidationTitleMsg?: string;
84
95
  errorValidationDenialOrderBlockingMsg?: string;
85
96
  errorValidationBlockingOffersMsg?: string;
86
- termsAndConditionsText?: string;
97
+ orderDenialOfferBaseText?: string;
98
+ orderDenialOfferWithFallbackText?: string;
99
+ orderDenialAmountText?: string;
100
+ orderDenialFallbackOffer?: OrderDenialFallbackOfferType;
101
+ termsAndConditionsText?: string | ReactNode;
87
102
  };
88
103
  };
89
104
 
package/tsconfig.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "es5",
3
+ "target": "es2020",
4
4
  "lib": ["dom", "dom.iterable", "esnext"],
5
- "downlevelIteration": true,
6
5
  "allowJs": true,
7
6
  "checkJs": false,
8
7
  "skipLibCheck": true,
@@ -12,28 +11,26 @@
12
11
  "forceConsistentCasingInFileNames": true,
13
12
  "noFallthroughCasesInSwitch": true,
14
13
  "module": "esnext",
15
- "moduleResolution": "node",
16
14
  "resolveJsonModule": true,
17
15
  "isolatedModules": false,
18
16
  "noEmit": false,
19
17
  "noImplicitAny": true,
20
18
  "jsx": "react-jsx",
21
- "baseUrl": "src",
22
19
  "sourceMap": true,
23
20
  "inlineSources": true,
24
21
  "sourceRoot": "/",
25
22
  "paths": {
26
- "*": ["*"],
27
- "API": ["api"],
28
- "API/*": ["api/*"],
29
- "Component/*": ["components/*"],
30
- "Utils/*": ["utils/*"],
31
- "Hooks/*": ["hooks/*"],
32
- "Models/*": ["models/*"],
33
- "Types/*": ["types/*"],
34
- "Enums/*": ["enums/*"],
35
- "Constants/*": ["constants/*"],
36
- "Assets/*": ["assets/*"]
23
+ "*": ["./src/*"],
24
+ "API": ["./src/api"],
25
+ "API/*": ["./src/api/*"],
26
+ "Component/*": ["./src/components/*"],
27
+ "Utils/*": ["./src/utils/*"],
28
+ "Hooks/*": ["./src/hooks/*"],
29
+ "Models/*": ["./src/models/*"],
30
+ "Types/*": ["./src/types/*"],
31
+ "Enums/*": ["./src/enums/*"],
32
+ "Constants/*": ["./src/constants/*"],
33
+ "Assets/*": ["./src/assets/*"]
37
34
  }
38
35
  },
39
36
  "include": ["src"],