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

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/README.md CHANGED
@@ -133,7 +133,11 @@ type ConfigType = {
133
133
  errorValidationBlockingOffersMsg?: string;
134
134
  paymentMethodNotAllowedMsg?: string;
135
135
  invoiceLookupNotFoundText?: string;
136
- termsAndConditionsText?: string;
136
+ orderDenialOfferBaseText?: string;
137
+ orderDenialOfferWithFallbackText?: string;
138
+ orderDenialAmountText?: string;
139
+ fetchDenialFallbackOffer?: (organizationId: string) => Promise<OrderDenialFallbackOfferType | undefined>;
140
+ termsAndConditionsText?: string | ReactNode;
137
141
  };
138
142
  }
139
143
  ```
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.14",
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,12 @@ 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.',
71
77
  paymentMethodNotAllowedMsg: '',
72
78
  invoiceLookupNotFoundText:
73
79
  'There was no recipient found for the given information',
@@ -344,6 +350,12 @@ const App: FC<ConfigType> = ({
344
350
  callback={handleInvoiceForm}
345
351
  backButtonText={settings?.backButtonText}
346
352
  verifyButtonText={settings?.verifyButtonText}
353
+ orderDenialCloseButtonText={
354
+ settings?.orderDenialCloseButtonText
355
+ }
356
+ orderDenialContinueButtonText={
357
+ settings?.orderDenialContinueButtonText
358
+ }
347
359
  organizationNumberLabel={settings?.cvrLabel}
348
360
  glnLabel={settings?.glnLabel}
349
361
  organizationNumber={
@@ -377,6 +389,18 @@ const App: FC<ConfigType> = ({
377
389
  errorValidationBlockingOffersMsg={
378
390
  settings?.errorValidationBlockingOffersMsg
379
391
  }
392
+ orderDenialOfferBaseText={
393
+ settings?.orderDenialOfferBaseText
394
+ }
395
+ orderDenialOfferWithFallbackText={
396
+ settings?.orderDenialOfferWithFallbackText
397
+ }
398
+ orderDenialAmountText={
399
+ settings?.orderDenialAmountText
400
+ }
401
+ fetchDenialFallbackOffer={
402
+ settings?.fetchDenialFallbackOffer
403
+ }
380
404
  />
381
405
  )}
382
406
  {formType === FormTypeEnum.EHF && (
@@ -385,6 +409,12 @@ const App: FC<ConfigType> = ({
385
409
  callback={handleInvoiceForm}
386
410
  backButtonText={settings?.backButtonText}
387
411
  verifyButtonText={settings?.verifyButtonText}
412
+ orderDenialCloseButtonText={
413
+ settings?.orderDenialCloseButtonText
414
+ }
415
+ orderDenialContinueButtonText={
416
+ settings?.orderDenialContinueButtonText
417
+ }
388
418
  organizationNumberLabel={
389
419
  settings?.organizationNumberLabel
390
420
  }
@@ -419,6 +449,18 @@ const App: FC<ConfigType> = ({
419
449
  errorValidationBlockingOffersMsg={
420
450
  settings?.errorValidationBlockingOffersMsg
421
451
  }
452
+ orderDenialOfferBaseText={
453
+ settings?.orderDenialOfferBaseText
454
+ }
455
+ orderDenialOfferWithFallbackText={
456
+ settings?.orderDenialOfferWithFallbackText
457
+ }
458
+ orderDenialAmountText={
459
+ settings?.orderDenialAmountText
460
+ }
461
+ fetchDenialFallbackOffer={
462
+ settings?.fetchDenialFallbackOffer
463
+ }
422
464
  />
423
465
  )}
424
466
  {formType !== FormTypeEnum.OIO &&
@@ -440,6 +482,12 @@ const App: FC<ConfigType> = ({
440
482
  submitButtonText={
441
483
  settings?.submitButtonText
442
484
  }
485
+ orderDenialCloseButtonText={
486
+ settings?.orderDenialCloseButtonText
487
+ }
488
+ orderDenialContinueButtonText={
489
+ settings?.orderDenialContinueButtonText
490
+ }
443
491
  paymentMethodLabel={
444
492
  settings?.paymentMethodLabel
445
493
  }
@@ -475,6 +523,18 @@ const App: FC<ConfigType> = ({
475
523
  errorValidationBlockingOffersMsg={
476
524
  settings?.errorValidationBlockingOffersMsg
477
525
  }
526
+ orderDenialOfferBaseText={
527
+ settings?.orderDenialOfferBaseText
528
+ }
529
+ orderDenialOfferWithFallbackText={
530
+ settings?.orderDenialOfferWithFallbackText
531
+ }
532
+ orderDenialAmountText={
533
+ settings?.orderDenialAmountText
534
+ }
535
+ fetchDenialFallbackOffer={
536
+ settings?.fetchDenialFallbackOffer
537
+ }
478
538
  termsAndConditionsText={
479
539
  settings?.termsAndConditionsText
480
540
  }
@@ -497,7 +557,9 @@ const App: FC<ConfigType> = ({
497
557
  {isFailed && <Alert className="mt-2" msg={failedMsg} />}
498
558
  {isFailed &&
499
559
  errorsMsg?.length > 0 &&
500
- errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
560
+ errorsMsg.map((i, index) => (
561
+ <Alert key={`${i}-${index}`} className="mt-2" msg={i} />
562
+ ))}
501
563
  </div>
502
564
  </ErrorBoundary>
503
565
  );
@@ -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
+ fetchDenialFallbackOffer?: (organizationId: string) => Promise<OrderDenialFallbackOfferType | undefined>;
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
+ fetchDenialFallbackOffer,
91
105
  }) => {
92
106
  const methods = useForm<EHFFormInputsType>({
93
107
  defaultValues: {
@@ -108,7 +122,17 @@ 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
+ orderDenialFallbackOffer,
134
+ dismissOrderDenial,
135
+ } = useOrderForm({
112
136
  callback,
113
137
  submitStartCallback,
114
138
  userActionCallback,
@@ -130,6 +154,10 @@ const EHFForm: FC<Props> = ({
130
154
  errorValidationTitleMsg,
131
155
  errorValidationDenialOrderBlockingMsg,
132
156
  errorValidationBlockingOffersMsg,
157
+ orderDenialOfferBaseText,
158
+ orderDenialOfferWithFallbackText,
159
+ orderDenialAmountText,
160
+ fetchDenialFallbackOffer,
133
161
  });
134
162
 
135
163
  const onSubmit: SubmitHandler<EHFFormInputsType> = async (
@@ -178,7 +206,27 @@ const EHFForm: FC<Props> = ({
178
206
 
179
207
  {apiErrorMsg && <Alert className="mt-2" msg={apiErrorMsg} />}
180
208
  {errorsMsg?.length > 0 &&
181
- errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
209
+ errorsMsg.map((i, index) => (
210
+ <Alert key={index} className="mt-2" msg={i} />
211
+ ))}
212
+ <OrderDenialModal
213
+ isOpen={!!orderDenialType}
214
+ message={orderDenialMessage}
215
+ closeButtonText={orderDenialCloseButtonText}
216
+ continueButtonText={orderDenialContinueButtonText}
217
+ canContinue={
218
+ orderDenialType === 'offer' &&
219
+ !!orderDenialFallbackOffer?.templatePackageId
220
+ }
221
+ offer={
222
+ orderDenialType === 'offer' &&
223
+ !!orderDenialFallbackOffer?.templatePackageId
224
+ ? orderDenialFallbackOffer
225
+ : undefined
226
+ }
227
+ onClose={dismissOrderDenial}
228
+ onContinue={continueWithFallbackOffer}
229
+ />
182
230
  </form>
183
231
  </FormProvider>
184
232
  );
@@ -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
+ fetchDenialFallbackOffer?: (organizationId: string) => Promise<OrderDenialFallbackOfferType | undefined>;
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
+ fetchDenialFallbackOffer,
95
109
  }) => {
96
110
  const methods = useForm<OIOFormInputsType>({
97
111
  defaultValues: {
@@ -116,7 +130,17 @@ 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
+ orderDenialFallbackOffer,
142
+ dismissOrderDenial,
143
+ } = useOrderForm({
120
144
  callback,
121
145
  submitStartCallback,
122
146
  userActionCallback,
@@ -138,6 +162,10 @@ const EHFForm: FC<Props> = ({
138
162
  errorValidationTitleMsg,
139
163
  errorValidationDenialOrderBlockingMsg,
140
164
  errorValidationBlockingOffersMsg,
165
+ orderDenialOfferBaseText,
166
+ orderDenialOfferWithFallbackText,
167
+ orderDenialAmountText,
168
+ fetchDenialFallbackOffer,
141
169
  });
142
170
 
143
171
  const onSubmit: SubmitHandler<OIOFormInputsType> = async (
@@ -189,10 +217,30 @@ const EHFForm: FC<Props> = ({
189
217
  </div>
190
218
  {apiErrorMsg && <Alert className="mt-2" msg={apiErrorMsg} />}
191
219
  {errorsMsg?.length > 0 &&
192
- errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
220
+ errorsMsg.map((i, index) => (
221
+ <Alert key={`${i}-${index}`} className="mt-2" msg={i} />
222
+ ))}
223
+ <OrderDenialModal
224
+ isOpen={!!orderDenialType}
225
+ message={orderDenialMessage}
226
+ closeButtonText={orderDenialCloseButtonText}
227
+ continueButtonText={orderDenialContinueButtonText}
228
+ canContinue={
229
+ orderDenialType === 'offer' &&
230
+ !!orderDenialFallbackOffer?.templatePackageId
231
+ }
232
+ offer={
233
+ orderDenialType === 'offer' &&
234
+ !!orderDenialFallbackOffer?.templatePackageId
235
+ ? orderDenialFallbackOffer
236
+ : undefined
237
+ }
238
+ onClose={dismissOrderDenial}
239
+ onContinue={continueWithFallbackOffer}
240
+ />
193
241
  </form>
194
242
  </FormProvider>
195
243
  );
196
244
  };
197
245
 
198
- export default EHFForm;
246
+ export default OIOForm;
@@ -0,0 +1,89 @@
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
46
+ className="sdk-order-denial-modal-text"
47
+ style={{ whiteSpace: 'pre-line' }}
48
+ >
49
+ {message?.replace(/\\n/g, '\n')}
50
+ </p>
51
+ {offer?.title && offer?.templatePackageId && (
52
+ <div className="sdk-order-denial-offer-box">
53
+ <h4 className="sdk-order-denial-offer-title">
54
+ {offer.title}
55
+ </h4>
56
+ {offer.description && (
57
+ <p className="sdk-order-denial-offer-description">
58
+ {offer.description}
59
+ </p>
60
+ )}
61
+ {offer.price && (
62
+ <p className="sdk-order-denial-offer-meta">
63
+ {offer.price}
64
+ </p>
65
+ )}
66
+ </div>
67
+ )}
68
+ <div className="sdk-order-denial-modal-actions">
69
+ <Button
70
+ type="button"
71
+ btnType="default"
72
+ buttonText={closeButtonText}
73
+ onClick={onClose}
74
+ />
75
+ {canContinue && (
76
+ <Button
77
+ type="button"
78
+ buttonText={continueButtonText}
79
+ onClick={onContinue}
80
+ />
81
+ )}
82
+ </div>
83
+ </div>
84
+ </div>,
85
+ document.body
86
+ );
87
+ };
88
+
89
+ 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,13 @@ 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
+ fetchDenialFallbackOffer?: (
80
+ organizationId: string
81
+ ) => Promise<OrderDenialFallbackOfferType | undefined>;
82
+ termsAndConditionsText?: string | ReactNode;
65
83
  };
66
84
 
67
85
  const initialData = {
@@ -98,6 +116,8 @@ const OrderForm: FC<Props> = ({
98
116
  allowedPaymentMethods,
99
117
  invoiceAddressSelection,
100
118
  submitButtonText = 'Start',
119
+ orderDenialCloseButtonText = 'Close',
120
+ orderDenialContinueButtonText = 'Continue',
101
121
  paymentMethodLabel = 'Select Payment Method',
102
122
  paymentMethodNotAllowedMsg = 'This payment method not allowed!',
103
123
  errorReqMsg = '',
@@ -106,6 +126,10 @@ const OrderForm: FC<Props> = ({
106
126
  errorValidationTitleMsg,
107
127
  errorValidationDenialOrderBlockingMsg,
108
128
  errorValidationBlockingOffersMsg,
129
+ orderDenialOfferBaseText,
130
+ orderDenialOfferWithFallbackText,
131
+ orderDenialAmountText,
132
+ fetchDenialFallbackOffer,
109
133
  requireTermsAcceptance,
110
134
  termsAndConditionsText,
111
135
  }) => {
@@ -175,7 +199,17 @@ const OrderForm: FC<Props> = ({
175
199
  invoiceAddressSelection?.paymentMethods ||
176
200
  INVOICE_ALLOWED_PAYMENT_METHODS;
177
201
 
178
- const { orderSubmit, loading, apiErrorMsg, errorsMsg } = useOrderForm({
202
+ const {
203
+ orderSubmit,
204
+ continueWithFallbackOffer,
205
+ loading,
206
+ apiErrorMsg,
207
+ errorsMsg,
208
+ orderDenialType,
209
+ orderDenialMessage,
210
+ orderDenialFallbackOffer,
211
+ dismissOrderDenial,
212
+ } = useOrderForm({
179
213
  callback,
180
214
  submitStartCallback,
181
215
  userActionCallback,
@@ -196,6 +230,10 @@ const OrderForm: FC<Props> = ({
196
230
  errorValidationTitleMsg,
197
231
  errorValidationDenialOrderBlockingMsg,
198
232
  errorValidationBlockingOffersMsg,
233
+ orderDenialOfferBaseText,
234
+ orderDenialOfferWithFallbackText,
235
+ orderDenialAmountText,
236
+ fetchDenialFallbackOffer,
199
237
  });
200
238
 
201
239
  const onSubmit: SubmitHandler<OrderFormInputsType> = async (
@@ -380,7 +418,27 @@ const OrderForm: FC<Props> = ({
380
418
  />
381
419
  {errorsMsg?.length > 0 &&
382
420
  allowPaymentMethod &&
383
- errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
421
+ errorsMsg.map((i, index) => (
422
+ <Alert key={`${i}-${index}`} className="mt-2" msg={i} />
423
+ ))}
424
+ <OrderDenialModal
425
+ isOpen={!!orderDenialType}
426
+ message={orderDenialMessage}
427
+ closeButtonText={orderDenialCloseButtonText}
428
+ continueButtonText={orderDenialContinueButtonText}
429
+ canContinue={
430
+ orderDenialType === 'offer' &&
431
+ !!orderDenialFallbackOffer?.templatePackageId
432
+ }
433
+ offer={
434
+ orderDenialType === 'offer' &&
435
+ !!orderDenialFallbackOffer?.templatePackageId
436
+ ? orderDenialFallbackOffer
437
+ : undefined
438
+ }
439
+ onClose={dismissOrderDenial}
440
+ onContinue={continueWithFallbackOffer}
441
+ />
384
442
  </form>
385
443
  </FormProvider>
386
444
  );
@@ -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,54 @@ type Props = {
55
54
  errorValidationTitleMsg?: string;
56
55
  errorValidationDenialOrderBlockingMsg?: string;
57
56
  errorValidationBlockingOffersMsg?: string;
57
+ orderDenialOfferBaseText?: string;
58
+ orderDenialOfferWithFallbackText?: string;
59
+ orderDenialAmountText?: string;
60
+ fetchDenialFallbackOffer?: (
61
+ organizationId: string
62
+ ) => Promise<OrderDenialFallbackOfferType | undefined>;
63
+ };
64
+
65
+ type DenialErrorType = {
66
+ status?: number;
67
+ errors?: Record<string, string[] | undefined>;
68
+ };
69
+
70
+ const OFFER_DENIAL_BASE_TEXT =
71
+ 'Hello again!\n\nLooks like you already tried this offer.';
72
+ const OFFER_DENIAL_WITH_FALLBACK_TEXT =
73
+ '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:';
74
+
75
+ const getDenialErrorType = (
76
+ error: DenialErrorType
77
+ ): 'offer' | 'amount' | null => {
78
+ if (error?.status !== 400 || !error?.errors) {
79
+ return null;
80
+ }
81
+
82
+ const normalizedEntries = Object.entries(error.errors).map(
83
+ ([key, value]) => [key.toLowerCase(), value] as const
84
+ );
85
+
86
+ const hasOffer = normalizedEntries.some(
87
+ ([key, value]) =>
88
+ key === 'offer' && Array.isArray(value) && value.length > 0
89
+ );
90
+
91
+ if (hasOffer) {
92
+ return 'offer';
93
+ }
94
+
95
+ const hasAmount = normalizedEntries.some(
96
+ ([key, value]) =>
97
+ key === 'amount' && Array.isArray(value) && value.length > 0
98
+ );
99
+
100
+ if (hasAmount) {
101
+ return 'amount';
102
+ }
103
+
104
+ return null;
58
105
  };
59
106
 
60
107
  const useOrderForm = ({
@@ -79,17 +126,60 @@ const useOrderForm = ({
79
126
  errorValidationTitleMsg,
80
127
  errorValidationDenialOrderBlockingMsg,
81
128
  errorValidationBlockingOffersMsg,
129
+ orderDenialOfferBaseText,
130
+ orderDenialOfferWithFallbackText,
131
+ orderDenialAmountText,
132
+ fetchDenialFallbackOffer,
82
133
  }: Props) => {
83
134
  const [loading, setLoading] = useState<boolean>(false);
84
135
  const [apiErrorMsg, setApiErrorMsg] = useState<string>('');
85
136
  const [errorsMsg, setErrorsMsg] = useState<string[]>([]);
137
+ const [orderDenialType, setOrderDenialType] = useState<
138
+ 'offer' | 'amount' | null
139
+ >(null);
140
+ const [orderDenialFallbackOffer, setOrderDenialFallbackOffer] = useState<
141
+ OrderDenialFallbackOfferType | undefined
142
+ >();
143
+ const [submittedOrderData, setSubmittedOrderData] =
144
+ useState<OrderFormInputsType | null>(null);
145
+ const fallbackOfferTemplatePackageIdRef = useRef<string | null>(null);
146
+
147
+ const dismissOrderDenial = () => {
148
+ setOrderDenialType(null);
149
+ setOrderDenialFallbackOffer(undefined);
150
+ };
151
+
152
+ const getOrderDenialMessage = () => {
153
+ if (orderDenialType === 'offer') {
154
+ if (orderDenialFallbackOffer?.templatePackageId) {
155
+ return (
156
+ orderDenialOfferWithFallbackText ||
157
+ OFFER_DENIAL_WITH_FALLBACK_TEXT
158
+ );
159
+ }
160
+
161
+ return orderDenialOfferBaseText || OFFER_DENIAL_BASE_TEXT;
162
+ }
163
+
164
+ if (orderDenialType === 'amount') {
165
+ return (
166
+ orderDenialAmountText ||
167
+ errorValidationDenialOrderBlockingMsg ||
168
+ WRONG_MSG
169
+ );
170
+ }
171
+
172
+ return '';
173
+ };
86
174
 
87
175
  const orderSubmit: SubmitHandler<OrderFormInputsType> = async (
88
176
  data
89
177
  ): Promise<void> => {
90
178
  setApiErrorMsg('');
91
179
  setErrorsMsg([]);
180
+ setOrderDenialType(null);
92
181
  setLoading(true);
182
+ setSubmittedOrderData(data);
93
183
  try {
94
184
  let id: string | undefined = subscriberId;
95
185
  let invoiceResponse = null;
@@ -161,6 +251,10 @@ const useOrderForm = ({
161
251
  if (id) {
162
252
  const paymentMethod =
163
253
  data.paymentMethod || PAYMENT_METHOD_DEFAULT;
254
+ const selectedTemplateSubscriptionPlanId =
255
+ fallbackOfferTemplatePackageIdRef.current ||
256
+ templatePackageId;
257
+ fallbackOfferTemplatePackageIdRef.current = null;
164
258
 
165
259
  const agreements = prepareAgreementModel({
166
260
  paymentMethod,
@@ -186,7 +280,8 @@ const useOrderForm = ({
186
280
  subscriberId: id,
187
281
  subscriptionPlan: {
188
282
  organizationId,
189
- templateSubscriptionPlanId: templatePackageId,
283
+ templateSubscriptionPlanId:
284
+ selectedTemplateSubscriptionPlanId,
190
285
  },
191
286
  paymentAgreement: { ...agreements },
192
287
  invoiceContact: invoiceAddressToggle
@@ -216,6 +311,30 @@ const useOrderForm = ({
216
311
  errorValidationDenialOrderBlockingMsg,
217
312
  } as ErrorMessages;
218
313
 
314
+ const detectedDenialType = getDenialErrorType(
315
+ error as DenialErrorType
316
+ );
317
+ if (detectedDenialType) {
318
+ setApiErrorMsg('');
319
+ setErrorsMsg([]);
320
+ setOrderDenialFallbackOffer(undefined);
321
+ setOrderDenialType(detectedDenialType);
322
+ callback(null);
323
+ if (
324
+ detectedDenialType === 'offer' &&
325
+ fetchDenialFallbackOffer
326
+ ) {
327
+ void fetchDenialFallbackOffer(organizationId)
328
+ .then((offer) => {
329
+ setOrderDenialFallbackOffer(offer);
330
+ })
331
+ .catch(() => {
332
+ setOrderDenialFallbackOffer(undefined);
333
+ });
334
+ }
335
+ return;
336
+ }
337
+
219
338
  console.error(error);
220
339
  setApiErrorMsg(
221
340
  prepareErrorMessage(error as Error, undefined, translations)
@@ -236,11 +355,37 @@ const useOrderForm = ({
236
355
  }
237
356
  };
238
357
 
358
+ const continueWithFallbackOffer = async () => {
359
+ if (orderDenialType !== 'offer' || !submittedOrderData) {
360
+ return;
361
+ }
362
+
363
+ const fallbackTemplatePackageId =
364
+ orderDenialFallbackOffer?.templatePackageId;
365
+
366
+ if (!fallbackTemplatePackageId) {
367
+ dismissOrderDenial();
368
+ return;
369
+ }
370
+
371
+ fallbackOfferTemplatePackageIdRef.current = fallbackTemplatePackageId;
372
+ userActionCallback?.(UserActionEnum.SELECT_FALLBACK_OFFER, {
373
+ templatePackageId: fallbackTemplatePackageId,
374
+ });
375
+ dismissOrderDenial();
376
+ await orderSubmit(submittedOrderData);
377
+ };
378
+
239
379
  return {
240
380
  orderSubmit,
381
+ continueWithFallbackOffer,
241
382
  loading,
242
383
  apiErrorMsg,
243
384
  errorsMsg,
385
+ orderDenialType,
386
+ orderDenialMessage: getOrderDenialMessage(),
387
+ orderDenialFallbackOffer,
388
+ dismissOrderDenial,
244
389
  };
245
390
  };
246
391
 
@@ -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,13 @@ 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
+ fetchDenialFallbackOffer?: (
101
+ organizationId: string
102
+ ) => Promise<OrderDenialFallbackOfferType | undefined>;
103
+ termsAndConditionsText?: string | ReactNode;
87
104
  };
88
105
  };
89
106
 
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"],