@infosoftas/s4-orders-js-sdk 0.0.1-alpha1b → 0.0.1-alpha1e

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.
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://unpkg.com/release-it/schema/release-it.json",
3
+ "github": {
4
+ "release": true
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@infosoftas/s4-orders-js-sdk",
3
- "version": "0.0.1-alpha1b",
3
+ "version": "0.0.1-alpha1e",
4
4
  "scripts": {
5
- "build": "rimraf dist && webpack --env mode=production"
5
+ "build": "rimraf lib && webpack --env mode=production"
6
6
  },
7
7
  "eslintConfig": {
8
8
  "extends": [
@@ -13,8 +13,7 @@
13
13
  "prop-types": "^15.8.1",
14
14
  "react": "^18.3.1",
15
15
  "react-dom": "^18.3.1",
16
- "react-hook-form": "^7.53.0",
17
- "rimraf": "^6.0.1"
16
+ "react-hook-form": "^7.53.0"
18
17
  },
19
18
  "devDependencies": {
20
19
  "@babel/core": "^7.24.3",
@@ -25,6 +24,7 @@
25
24
  "@types/react-dom": "^18.0.6",
26
25
  "babel-loader": "^8.3.0",
27
26
  "css-loader": "^7.1.2",
27
+ "copy-webpack-plugin": "^12.0.2",
28
28
  "dotenv-webpack": "^8.1.0",
29
29
  "mini-css-extract-plugin": "^2.9.1",
30
30
  "sass": "^1.79.2",
@@ -33,7 +33,8 @@
33
33
  "ts-loader": "^9.5.1",
34
34
  "typescript": "^5.6.2",
35
35
  "webpack": "^5.95.0",
36
- "webpack-cli": "^5.1.4"
36
+ "webpack-cli": "^5.1.4",
37
+ "rimraf": "^6.0.1"
37
38
  },
38
39
  "publishConfig": {
39
40
  "access": "restricted"
package/src/App.scss CHANGED
@@ -1,5 +1,5 @@
1
- @import 'Assets/styles/variables.scss';
2
- @import 'Assets/styles/general.scss';
1
+ @import './assets/styles/variables.scss';
2
+ @import './assets/styles/general.scss';
3
3
 
4
4
  .sdk-order-container {
5
5
 
package/src/App.tsx CHANGED
@@ -1,15 +1,15 @@
1
1
  import { FC, useEffect, useState } from 'react';
2
2
 
3
- import { MessageEventTypeEnum, PaymentMethodEnum } from 'Enums/general';
4
- import useMessageEvent from 'Hooks/useMessageEvent';
5
- import OrderForm from 'Component/OrderForm/OrderForm';
6
- import Loader from 'Component/Loader/Loader';
7
- import MainIframe from 'Component/MainIframe/MainIframe';
8
- import Alert from 'Component/Alert/Alert';
9
- import { ConfigType } from 'Types/general';
10
- import { CompleteOrderParamsType, OrderInfoType } from 'Types/order';
11
- import { orderComplete } from 'API/OrdersApi';
12
- import { prepareErrorMessage } from 'Utils/helper';
3
+ import { MessageEventTypeEnum, PaymentMethodEnum } from './enums/general';
4
+ import useMessageEvent from './hooks/useMessageEvent';
5
+ import OrderForm from './components/OrderForm/OrderForm';
6
+ import Loader from './components/Loader/Loader';
7
+ import MainIframe from './components/MainIframe/MainIframe';
8
+ import Alert from './components/Alert/Alert';
9
+ import { ConfigType, ErrorsMsg } from './types/general';
10
+ import { CompleteOrderParamsType, OrderInfoType } from './types/order';
11
+ import { orderComplete, orderDelete } from './api/OrdersApi';
12
+ import { prepareErrorMessage, prepareErrorsArrayMessage } from './utils/helper';
13
13
 
14
14
  import ErrorBoundary from './ErrorBoundary';
15
15
 
@@ -18,6 +18,7 @@ import './App.scss';
18
18
  const App: FC<ConfigType> = ({
19
19
  moduleTitle,
20
20
  apiKey,
21
+ apiUrl,
21
22
  templatePackageId,
22
23
  subscriberId,
23
24
  userId,
@@ -42,6 +43,7 @@ const App: FC<ConfigType> = ({
42
43
  const [isConfirmed, setIsConfirmed] = useState<boolean>(false);
43
44
  const [isFailed, setIsFailed] = useState<boolean>(false);
44
45
  const [failedMsg, setFailedMsg] = useState<string>('');
46
+ const [errorsMsg, setErrorsMsg] = useState<string[]>([]);
45
47
  const [orderInfo, setOrderInfo] = useState<OrderInfoType | null>(null);
46
48
 
47
49
  useEffect(() => {
@@ -50,6 +52,12 @@ const App: FC<ConfigType> = ({
50
52
  }
51
53
  }, [apiKey]);
52
54
 
55
+ useEffect(() => {
56
+ if (apiUrl) {
57
+ sessionStorage.setItem('sdk_api_url', apiUrl);
58
+ }
59
+ }, [apiUrl]);
60
+
53
61
  const messageCallback = async (
54
62
  data: CompleteOrderParamsType
55
63
  ): Promise<void> => {
@@ -57,6 +65,8 @@ const App: FC<ConfigType> = ({
57
65
  setIframeSrc(null);
58
66
  setShowOrderForm(false);
59
67
  setIsFailed(false);
68
+ setFailedMsg('');
69
+ setErrorsMsg([]);
60
70
  try {
61
71
  await orderComplete(orderId || data.orderId || '');
62
72
  setIsConfirmed(true);
@@ -73,23 +83,83 @@ const App: FC<ConfigType> = ({
73
83
  }
74
84
  } catch (error) {
75
85
  console.log(error);
86
+ if (
87
+ (error as { status: number }).status === 409 &&
88
+ (data.paymentMethod === PaymentMethodEnum.Vipps ||
89
+ data.paymentMethod === PaymentMethodEnum.MobilePay)
90
+ ) {
91
+ handleOrderDelete(orderId || data.orderId);
92
+ return;
93
+ }
94
+
76
95
  setFailedMsg(
77
96
  prepareErrorMessage(error as Error, settings?.failureText)
78
97
  );
98
+ setErrorsMsg(
99
+ prepareErrorsArrayMessage(
100
+ (
101
+ error as {
102
+ errors: ErrorsMsg;
103
+ }
104
+ )?.errors
105
+ )
106
+ );
79
107
  setIsFailed(true);
80
108
  setLoading(false);
81
109
  setShowOrderForm(true);
82
110
  }
83
111
  };
84
112
 
85
- const handleMessageEvent = (type: MessageEventTypeEnum) => {
113
+ const handleOrderDelete = async (id: string) => {
114
+ try {
115
+ await orderDelete(id);
116
+ if (window === top) {
117
+ top.postMessage(
118
+ {
119
+ type: MessageEventTypeEnum.ORDER_FLOW_CANCEL,
120
+ isCanceled: true,
121
+ orderInfo: orderInfo,
122
+ },
123
+ top?.location?.origin || '*'
124
+ );
125
+ }
126
+ setIsFailed(false);
127
+ } catch (error) {
128
+ console.log(error);
129
+ setFailedMsg(
130
+ prepareErrorMessage(error as Error, settings?.failureText)
131
+ );
132
+ setErrorsMsg(
133
+ prepareErrorsArrayMessage(
134
+ (
135
+ error as {
136
+ errors: ErrorsMsg;
137
+ }
138
+ )?.errors
139
+ )
140
+ );
141
+ setIsFailed(true);
142
+ }
143
+
144
+ setLoading(false);
145
+ setShowOrderForm(true);
146
+ };
147
+
148
+ const handleMessageEvent = async (type: MessageEventTypeEnum) => {
86
149
  if (type === MessageEventTypeEnum.CANCEL) {
150
+ try {
151
+ if (orderId) {
152
+ await handleOrderDelete(orderId);
153
+ }
154
+ } catch (error) {
155
+ console.log(error);
156
+ }
157
+
87
158
  setLoading(false);
88
159
  setOrderId(null);
89
160
  setOrderInfo(null);
90
161
  setIframeSrc(null);
91
162
  setIsConfirmed(false);
92
- setIsFailed(false);
93
163
  setShowOrderForm(true);
94
164
  }
95
165
  };
@@ -106,6 +176,16 @@ const App: FC<ConfigType> = ({
106
176
  setIsFailed(false);
107
177
  setOrderId(data?.orderId || null);
108
178
  setOrderInfo(data || null);
179
+ if (window === top) {
180
+ top.postMessage(
181
+ {
182
+ type: MessageEventTypeEnum.ORDER_UPDATE_INFO,
183
+ isUpdate: true,
184
+ orderInfo: data || null,
185
+ },
186
+ top?.location?.origin || '*'
187
+ );
188
+ }
109
189
  if (url) {
110
190
  if (
111
191
  showIframe &&
@@ -155,12 +235,8 @@ const App: FC<ConfigType> = ({
155
235
  errorInvalidEmailMsg={settings?.errorInvalidEmailMsg}
156
236
  errorInvalidPhoneMsg={settings?.errorInvalidPhoneMsg}
157
237
  defaultValues={settings?.orderDefaultValues}
158
- redirectUrl={
159
- showIframe
160
- ? window.location.href.split('?')[0]
161
- : redirectUrl ||
162
- window.location.href.split('?')[0]
163
- }
238
+ redirectUrl={redirectUrl}
239
+ showIframe={showIframe}
164
240
  paymentMethodsOptions={paymentMethodsOptions}
165
241
  language={language}
166
242
  merchantAgreementUrl={merchantAgreementUrl}
@@ -176,6 +252,9 @@ const App: FC<ConfigType> = ({
176
252
  )}
177
253
 
178
254
  {isFailed && <Alert className="mt-2" msg={failedMsg} />}
255
+ {isFailed &&
256
+ errorsMsg?.length > 0 &&
257
+ errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
179
258
  </div>
180
259
  </ErrorBoundary>
181
260
  );
@@ -1,5 +1,5 @@
1
- import { AgreementsType } from 'Types/order';
2
- import fetcher from 'Utils/fetcher';
1
+ import { AgreementsType } from '../types/order';
2
+ import fetcher from '../utils/fetcher';
3
3
 
4
4
  type RequestOrderStartType = {
5
5
  subscriberId?: string;
@@ -42,3 +42,16 @@ export const orderComplete = (id: string): Promise<OrderResponseType> =>
42
42
  },
43
43
  body: null,
44
44
  }) as Promise<OrderResponseType>;
45
+
46
+ export const orderDelete = (id: string): Promise<OrderResponseType> =>
47
+ fetcher<OrderResponseType, null>({
48
+ method: 'DELETE',
49
+ url: `/order/${id}`,
50
+ headers: {
51
+ 'S4-ORDERS-API-KEY':
52
+ sessionStorage.getItem('sdk_api_key') ||
53
+ process.env.API_KEY ||
54
+ '',
55
+ },
56
+ body: null,
57
+ }) as Promise<OrderResponseType>;
@@ -1,9 +1,13 @@
1
- import fetcher from 'Utils/fetcher';
1
+ import fetcher from '../utils/fetcher';
2
2
 
3
3
  type CreateSubscriberRequestType = {
4
4
  name?: string;
5
5
  email?: string;
6
6
  phone?: string;
7
+ country?: string;
8
+ city?: string;
9
+ addressLines?: string[];
10
+ zip?: string;
7
11
  };
8
12
 
9
13
  type CreateSubscriberResponseType = {
@@ -1,4 +1,4 @@
1
- @import 'Assets/styles/variables.scss';
1
+ @import './variables.scss';
2
2
 
3
3
  .field-label {
4
4
  display : block;
@@ -1,5 +1,5 @@
1
- @import 'Assets/styles/variables.scss';
2
- @import 'Assets/styles/forms.scss';
1
+ @import './variables.scss';
2
+ @import './forms.scss';
3
3
 
4
4
  .d-flex {
5
5
  display: flex;
@@ -1,4 +1,4 @@
1
- @import 'Assets/styles/variables.scss';
1
+ @import '../../assets/styles/variables.scss';
2
2
 
3
3
  .sdk-alert {
4
4
  position : relative;
@@ -1,6 +1,6 @@
1
1
  import { FC, MouseEvent } from 'react';
2
2
 
3
- import Loader from 'Component/Loader/Loader';
3
+ import Loader from '../Loader/Loader';
4
4
 
5
5
  import './button.scss';
6
6
 
@@ -1,6 +1,6 @@
1
1
  @use "sass:color";
2
2
 
3
- @import 'Assets/styles/variables.scss';
3
+ @import '../../assets/styles/variables.scss';
4
4
 
5
5
  .sdk-btn {
6
6
  appearance : button;
@@ -0,0 +1,52 @@
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
+ address?: {
14
+ message?: string;
15
+ };
16
+ };
17
+ };
18
+
19
+ const AddressField: FC<Props> = ({
20
+ required = false,
21
+ readOnly = false,
22
+ label = 'Address',
23
+ errorReqMsg = 'This field is required!',
24
+ errors,
25
+ }) => {
26
+ const { register } = useFormContext();
27
+ const id = uuidv4();
28
+ return (
29
+ <div className="field-wrapper" data-testid="sdk-address-field-id">
30
+ <textarea
31
+ id={id}
32
+ autoComplete="address"
33
+ placeholder=" "
34
+ readOnly={readOnly}
35
+ className={`input-control ${readOnly ? 'read-only' : ''}`}
36
+ {...register('address', {
37
+ required: required ? errorReqMsg : false,
38
+ })}
39
+ />
40
+ <label className="label-control" htmlFor={id}>
41
+ {label} {required && <span className="text-error">*</span>}
42
+ </label>
43
+ {errors && errors.address && (
44
+ <div className="text-error caption">
45
+ {errors.address.message}
46
+ </div>
47
+ )}
48
+ </div>
49
+ );
50
+ };
51
+
52
+ export default AddressField;
@@ -1,6 +1,8 @@
1
1
  import { FC } from 'react';
2
2
  import { useFormContext } from 'react-hook-form';
3
3
 
4
+ import { uuidv4 } from '../../utils/helper';
5
+
4
6
  type Props = {
5
7
  name: string;
6
8
  required: boolean;
@@ -24,9 +26,11 @@ const EmailField: FC<Props> = ({
24
26
  errors,
25
27
  }) => {
26
28
  const { register } = useFormContext();
29
+ const id = uuidv4();
27
30
  return (
28
31
  <div className="field-wrapper" data-testid="sdk-email-field-id">
29
32
  <input
33
+ id={id}
30
34
  autoComplete="email"
31
35
  placeholder=" "
32
36
  className={`input-control ${readOnly ? 'read-only' : ''}`}
@@ -40,7 +44,7 @@ const EmailField: FC<Props> = ({
40
44
  },
41
45
  })}
42
46
  />
43
- <label className="label-control">
47
+ <label className="label-control" htmlFor={id}>
44
48
  {label} {required && <span className="text-error">*</span>}
45
49
  </label>
46
50
  {errors && errors.email && (
@@ -1,9 +1,13 @@
1
- import { OrderFormFiledType } from 'Types/general';
1
+ import { OrderFormFiledType } from '../../types/general';
2
2
 
3
3
  export const FIELD_TYPES = {
4
4
  PHONE: 'phoneNumber',
5
5
  EMAIL: 'email',
6
6
  NAME: 'name',
7
+ COUNTRY: 'country',
8
+ CITY: 'city',
9
+ ADDRESS: 'address',
10
+ ZIP: 'zip',
7
11
  };
8
12
 
9
13
  export const DEFAULT_ORDER_FORM_FIELDS: OrderFormFiledType[] = [
@@ -1,6 +1,8 @@
1
- import EmailField from 'Component/FormFields/EmailField';
2
- import PhoneField from 'Component/FormFields/PhoneField';
3
- import NameField from 'Component/FormFields/NameField';
1
+ import EmailField from './EmailField';
2
+ import PhoneField from './PhoneField';
3
+ import NameField from './NameField';
4
+ import AddressField from './AddressField';
5
+ import InputField from './InputField';
4
6
 
5
7
  import { FIELD_TYPES } from './FormFields.helper';
6
8
 
@@ -8,6 +10,10 @@ const formFieldsMapper = {
8
10
  [FIELD_TYPES.PHONE]: PhoneField,
9
11
  [FIELD_TYPES.NAME]: NameField,
10
12
  [FIELD_TYPES.EMAIL]: EmailField,
13
+ [FIELD_TYPES.ADDRESS]: AddressField,
14
+ [FIELD_TYPES.COUNTRY]: InputField,
15
+ [FIELD_TYPES.CITY]: InputField,
16
+ [FIELD_TYPES.ZIP]: InputField,
11
17
  };
12
18
 
13
19
  export default formFieldsMapper;
@@ -0,0 +1,51 @@
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 NameField: 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" data-testid={`sdk-${name}-field-id`}>
31
+ <input
32
+ id={id}
33
+ autoComplete={name}
34
+ placeholder=" "
35
+ readOnly={readOnly}
36
+ className={`input-control ${readOnly ? 'read-only' : ''}`}
37
+ {...register(name, {
38
+ required: required ? errorReqMsg : false,
39
+ })}
40
+ />
41
+ <label className="label-control" htmlFor={id}>
42
+ {label} {required && <span className="text-error">*</span>}
43
+ </label>
44
+ {errors && errors[name] && (
45
+ <div className="text-error caption">{errors[name].message}</div>
46
+ )}
47
+ </div>
48
+ );
49
+ };
50
+
51
+ export default NameField;
@@ -1,6 +1,8 @@
1
1
  import { FC } from 'react';
2
2
  import { useFormContext } from 'react-hook-form';
3
3
 
4
+ import { uuidv4 } from '../../utils/helper';
5
+
4
6
  type Props = {
5
7
  name: string;
6
8
  required: boolean;
@@ -22,9 +24,11 @@ const NameField: FC<Props> = ({
22
24
  errors,
23
25
  }) => {
24
26
  const { register } = useFormContext();
27
+ const id = uuidv4();
25
28
  return (
26
29
  <div className="field-wrapper" data-testid="sdk-name-field-id">
27
30
  <input
31
+ id={id}
28
32
  autoComplete="name"
29
33
  placeholder=" "
30
34
  readOnly={readOnly}
@@ -33,7 +37,7 @@ const NameField: FC<Props> = ({
33
37
  required: required ? errorReqMsg : false,
34
38
  })}
35
39
  />
36
- <label className="label-control">
40
+ <label className="label-control" htmlFor={id}>
37
41
  {label} {required && <span className="text-error">*</span>}
38
42
  </label>
39
43
  {errors && errors.name && (
@@ -1,6 +1,8 @@
1
1
  import { FC } from 'react';
2
2
  import { useFormContext } from 'react-hook-form';
3
3
 
4
+ import { uuidv4 } from '../../utils/helper';
5
+
4
6
  type Props = {
5
7
  name: string;
6
8
  required: boolean;
@@ -24,9 +26,11 @@ const PhoneFiled: FC<Props> = ({
24
26
  errors,
25
27
  }) => {
26
28
  const { register } = useFormContext();
29
+ const id = uuidv4();
27
30
  return (
28
31
  <div className="field-wrapper" data-testid="sdk-phone-field-id">
29
32
  <input
33
+ id={id}
30
34
  autoComplete="tel"
31
35
  className={`input-control ${readOnly ? 'read-only' : ''}`}
32
36
  placeholder=" "
@@ -40,7 +44,7 @@ const PhoneFiled: FC<Props> = ({
40
44
  },
41
45
  })}
42
46
  />
43
- <label className="label-control">
47
+ <label className="label-control" htmlFor={id}>
44
48
  {label} {required && <span className="text-error">*</span>}
45
49
  </label>
46
50
  {errors && errors.phoneNumber && (
@@ -1,4 +1,4 @@
1
- @import 'Assets/styles/variables.scss';
1
+ @import '../../assets/styles/variables.scss';
2
2
 
3
3
  .sdk-loader {
4
4
  border : 2px solid $light-gray;
@@ -1,6 +1,6 @@
1
1
  import { FC, useEffect, useState } from 'react';
2
2
 
3
- import useIframe from 'Hooks/useIframe';
3
+ import useIframe from '../../hooks/useIframe';
4
4
 
5
5
  type Props = {
6
6
  iframeSrc: string | null;
@@ -1,18 +1,25 @@
1
1
  import { FC, ChangeEvent, Suspense, useState } from 'react';
2
2
  import { FormProvider, useForm, SubmitHandler } from 'react-hook-form';
3
3
 
4
- import { createSubscriber, mapSubscriberToUser } from 'API/SubscribeApi';
5
- import { orderStart } from 'API/OrdersApi';
6
- import { PaymentMethodEnum } from 'Enums/general';
7
- import { WRONG_MSG, PAYMENT_METHOD_DEFAULT } from 'Constants/index';
8
- import Alert from 'Component/Alert/Alert';
9
- import Button from 'Component/Button/Button';
10
- import formFieldsMapper from 'Component/FormFields/FormFieldsMapper';
11
- import { DEFAULT_ORDER_FORM_FIELDS } from 'Component/FormFields/FormFields.helper';
12
- import { OrderFormInputsType, OrderInfoType } from 'Types/order';
13
- import { PaymentMethodOptionsType, OrderFormFiledType } from 'Types/general';
14
- import { prepareAgreementModel } from 'Utils/order.helper';
15
- import { prepareErrorMessage } from 'Utils/helper';
4
+ import { createSubscriber, mapSubscriberToUser } from '../../api/SubscribeApi';
5
+ import { orderStart } from '../../api/OrdersApi';
6
+ import { PaymentMethodEnum } from '../../enums/general';
7
+ import { WRONG_MSG, PAYMENT_METHOD_DEFAULT } from '../../constants/index';
8
+ import Alert from '../Alert/Alert';
9
+ import Button from '../Button/Button';
10
+ import formFieldsMapper from '../FormFields/FormFieldsMapper';
11
+ import { DEFAULT_ORDER_FORM_FIELDS } from '../FormFields/FormFields.helper';
12
+ import { OrderFormInputsType, OrderInfoType } from '../../types/order';
13
+ import {
14
+ PaymentMethodOptionsType,
15
+ OrderFormFiledType,
16
+ ErrorsMsg,
17
+ } from '../../types/general';
18
+ import { prepareAgreementModel } from '../../utils/order.helper';
19
+ import {
20
+ prepareErrorMessage,
21
+ prepareErrorsArrayMessage,
22
+ } from '../../utils/helper';
16
23
 
17
24
  import './orderForm.scss';
18
25
 
@@ -23,7 +30,8 @@ type Props = {
23
30
  userId?: string;
24
31
  identityProviderId?: string;
25
32
  organizationId: string;
26
- redirectUrl: string;
33
+ redirectUrl?: string;
34
+ showIframe?: boolean;
27
35
  language: string;
28
36
  merchantAgreementUrl: string;
29
37
  paymentMethodsOptions?: PaymentMethodOptionsType;
@@ -40,6 +48,10 @@ const initialData = {
40
48
  name: '',
41
49
  email: '',
42
50
  phoneNumber: '',
51
+ country: '',
52
+ city: '',
53
+ address: '',
54
+ zip: '',
43
55
  paymentMethod: PAYMENT_METHOD_DEFAULT,
44
56
  };
45
57
 
@@ -51,6 +63,7 @@ const OrderForm: FC<Props> = ({
51
63
  identityProviderId,
52
64
  organizationId,
53
65
  redirectUrl,
66
+ showIframe,
54
67
  paymentMethodsOptions,
55
68
  defaultValues,
56
69
  language,
@@ -64,6 +77,7 @@ const OrderForm: FC<Props> = ({
64
77
  }) => {
65
78
  const [loading, setLoading] = useState<boolean>(false);
66
79
  const [apiErrorMsg, setApiErrorMsg] = useState<string>('');
80
+ const [errorsMsg, setErrorsMsg] = useState<string[]>([]);
67
81
  const [orderFields, setOrderFields] = useState<OrderFormFiledType[]>(
68
82
  paymentMethodsOptions?.[PAYMENT_METHOD_DEFAULT]?.orderFormFields ??
69
83
  DEFAULT_ORDER_FORM_FIELDS
@@ -87,15 +101,40 @@ const OrderForm: FC<Props> = ({
87
101
  data
88
102
  ): Promise<void> => {
89
103
  setApiErrorMsg('');
104
+ setErrorsMsg([]);
90
105
 
91
106
  setLoading(true);
92
107
  try {
93
108
  let id: string | undefined = subscriberId;
94
109
  if (!subscriberId) {
95
110
  const response = await createSubscriber({
96
- name: data.name,
97
- email: data.email,
98
- phone: data.phoneNumber,
111
+ name: orderFields.find((i) => i.name === 'name')
112
+ ? data.name?.trim() || undefined
113
+ : undefined,
114
+ email: orderFields.find((i) => i.name === 'email')
115
+ ? data.email?.trim() || undefined
116
+ : undefined,
117
+ phone: orderFields.find((i) => i.name === 'phoneNumber')
118
+ ? data.phoneNumber?.trim() || undefined
119
+ : undefined,
120
+ country: orderFields.find((i) => i.name === 'country')
121
+ ? data.country?.trim() || undefined
122
+ : undefined,
123
+ city: orderFields.find((i) => i.name === 'city')
124
+ ? data.city?.trim() || undefined
125
+ : undefined,
126
+ addressLines: orderFields.find((i) => i.name === 'address')
127
+ ? data.address?.trim()
128
+ ? data.address
129
+ ?.trim()
130
+ .replace(/\r\n/g, '\n')
131
+ .split('\n')
132
+ .filter((line) => line)
133
+ : undefined
134
+ : undefined,
135
+ zip: orderFields.find((i) => i.name === 'zip')
136
+ ? data.zip?.trim() || undefined
137
+ : undefined,
99
138
  });
100
139
  id = response.id;
101
140
 
@@ -114,7 +153,12 @@ const OrderForm: FC<Props> = ({
114
153
  const agreements = prepareAgreementModel({
115
154
  paymentMethod,
116
155
  redirectUrl,
117
- data,
156
+ showIframe,
157
+ phoneNumber: orderFields.find(
158
+ (i) => i.name === 'phoneNumber'
159
+ )
160
+ ? data.phoneNumber?.trim() || undefined
161
+ : undefined,
118
162
  language,
119
163
  merchantAgreementUrl,
120
164
  generateSubscriberContact:
@@ -143,9 +187,19 @@ const OrderForm: FC<Props> = ({
143
187
  }
144
188
  callback(null);
145
189
  setApiErrorMsg(WRONG_MSG);
190
+ setErrorsMsg([]);
146
191
  } catch (error) {
147
192
  console.error(error);
148
193
  setApiErrorMsg(prepareErrorMessage(error as Error));
194
+ setErrorsMsg(
195
+ prepareErrorsArrayMessage(
196
+ (
197
+ error as {
198
+ errors: ErrorsMsg;
199
+ }
200
+ )?.errors
201
+ )
202
+ );
149
203
  callback(null);
150
204
  } finally {
151
205
  setLoading(false);
@@ -227,6 +281,8 @@ const OrderForm: FC<Props> = ({
227
281
  buttonText={buttonText}
228
282
  />
229
283
  <Alert className="mt-2" msg={apiErrorMsg} />
284
+ {errorsMsg?.length > 0 &&
285
+ errorsMsg.map((i) => <Alert className="mt-2" msg={i} />)}
230
286
  </form>
231
287
  </FormProvider>
232
288
  );
@@ -1,4 +1,4 @@
1
- @import 'Assets/styles/variables.scss';
1
+ @import '../../assets/styles/variables.scss';
2
2
 
3
3
  .sdk-order-form {
4
4
  max-width : 320px;
@@ -1,4 +1,4 @@
1
- import { PaymentMethodEnum } from 'Enums/general';
1
+ import { PaymentMethodEnum } from '../enums/general';
2
2
 
3
3
  export const BASE_API = process.env.API_URL ?? '/';
4
4
 
@@ -6,6 +6,8 @@ export enum PaymentMethodEnum {
6
6
 
7
7
  export enum MessageEventTypeEnum {
8
8
  ORDER_FLOW_COMPLETE = 'orderFlowComplete',
9
+ ORDER_FLOW_CANCEL = 'orderFlowCancel',
10
+ ORDER_UPDATE_INFO = 'orderUpdateInfo',
9
11
  VIPPS_COMPLETE = 'completeOrder',
10
12
  CANCEL = 'cancel',
11
13
  COMPLETE = 'complete',
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useCallback } from 'react';
2
2
 
3
- import { MessageEventTypeEnum } from 'Enums/general';
4
- import { CompleteOrderParamsType, OrderInfoType } from 'Types/order';
3
+ import { MessageEventTypeEnum, PaymentMethodEnum } from '../enums/general';
4
+ import { CompleteOrderParamsType, OrderInfoType } from '../types/order';
5
5
  import useQueryParams from './useQueryParams';
6
6
 
7
7
  const useMessageEvent = (
@@ -76,6 +76,7 @@ const useMessageEvent = (
76
76
  orderId: queryParams.get('orderId') || '',
77
77
  agreementId: queryParams.get('agreementId') || '',
78
78
  orderInfo: orderInfo || null,
79
+ paymentMethod: PaymentMethodEnum.Vipps,
79
80
  });
80
81
  }
81
82
  }, [
package/src/index.tsx CHANGED
@@ -1,21 +1,22 @@
1
1
  import { createRoot, Root } from 'react-dom/client';
2
2
  import { createElement } from 'react';
3
3
 
4
- import { ConfigType } from 'Types/general';
5
- import { createSubscriber, mapSubscriberToUser } from 'API/SubscribeApi';
4
+ import { ConfigType } from './types/general';
5
+ import { createSubscriber, mapSubscriberToUser } from './api/SubscribeApi';
6
6
  import App from './App';
7
7
 
8
- let root: Root;
9
-
10
8
  export const orderComponent = {
9
+ root: null as Root | null,
11
10
  init: function (config: ConfigType) {
12
11
  const domNode = document.getElementById(config.domElementId);
13
- root = createRoot(domNode || document.createElement('div'));
12
+ orderComponent.root = createRoot(
13
+ domNode || document.createElement('div')
14
+ );
14
15
  const reactElement = createElement(App, config);
15
- root.render(reactElement);
16
+ orderComponent.root.render(reactElement);
16
17
  },
17
18
  remove: function () {
18
- (root as Root)?.unmount();
19
+ (orderComponent.root as Root)?.unmount();
19
20
  },
20
21
  createSubscriber,
21
22
  mapSubscriberToUser,
package/src/types/api.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HttpStatusCode } from 'Enums/api';
1
+ import { HttpStatusCode } from '../enums/api';
2
2
 
3
3
  export interface ApiResponseType<T> {
4
4
  status: HttpStatusCode;
@@ -1,4 +1,4 @@
1
- import { PaymentMethodEnum } from 'Enums/general';
1
+ import { PaymentMethodEnum } from '../enums/general';
2
2
  import { OrderFormInputsType } from './order';
3
3
 
4
4
  declare global {
@@ -31,6 +31,7 @@ export type ConfigType = {
31
31
  identityProviderId: string;
32
32
  organizationId: string;
33
33
  apiKey?: string;
34
+ apiUrl?: string;
34
35
  redirectUrl?: string;
35
36
  showIframe?: boolean;
36
37
  availablePaymentMethods?: { label: string; value: PaymentMethodEnum }[];
@@ -48,3 +49,7 @@ export type ConfigType = {
48
49
  errorInvalidPhoneMsg?: string;
49
50
  };
50
51
  };
52
+
53
+ export type ErrorsMsg = {
54
+ [key: string]: string[];
55
+ };
@@ -1,10 +1,14 @@
1
- import { PaymentMethodEnum } from 'Enums/general';
1
+ import { PaymentMethodEnum } from '../enums/general';
2
2
 
3
3
  export type OrderFormInputsType = {
4
4
  name?: string;
5
5
  email?: string;
6
6
  phoneNumber?: string;
7
7
  paymentMethod?: PaymentMethodEnum;
8
+ country?: string;
9
+ city?: string;
10
+ address?: string;
11
+ zip?: string;
8
12
  };
9
13
 
10
14
  export type VippsMobilePay = {
@@ -38,4 +42,5 @@ export type CompleteOrderParamsType = {
38
42
  orderId: string;
39
43
  agreementId: string;
40
44
  orderInfo: OrderInfoType | null;
45
+ paymentMethod?: PaymentMethodEnum | null;
41
46
  };
@@ -1,6 +1,6 @@
1
- import { HttpStatusCode } from 'Enums/api';
2
- import { ApiResponseType } from 'Types/api';
3
- import { BASE_API } from 'Constants/index';
1
+ import { HttpStatusCode } from '../enums/api';
2
+ import { ApiResponseType } from '../types/api';
3
+ import { BASE_API } from '../constants/index';
4
4
 
5
5
  export type Method =
6
6
  | 'GET'
@@ -42,7 +42,8 @@ async function fetcher<ResponseType, BodyType = void>(
42
42
  ): Promise<ApiResponseType<ResponseType> | ResponseType> {
43
43
  const { url, method, headers, body, params } = createHeader(options);
44
44
 
45
- const endpoint = new URL(`${BASE_API}${url}`);
45
+ const apiUrl = sessionStorage.getItem('sdk_api_url');
46
+ const endpoint = new URL(`${apiUrl || BASE_API}${url}`);
46
47
 
47
48
  if (params) {
48
49
  Object.entries(params).forEach(([key, value]) => {
@@ -68,12 +69,15 @@ async function fetcher<ResponseType, BodyType = void>(
68
69
  } else if (response.status === HttpStatusCode.FORBIDDEN) {
69
70
  reject({ message: 'Something Wrong with permission!' });
70
71
  } else {
71
- const data = await response?.json();
72
72
  if (response.ok) {
73
- resolve(data);
73
+ response
74
+ .json()
75
+ .then((data) => resolve(data))
76
+ .catch(() => resolve(null as ResponseType));
74
77
  } else {
78
+ const data = await response?.json();
75
79
  const message =
76
- data.message || data.details || data.title;
80
+ data.details || data.message || data.title;
77
81
  reject({ ...data, message });
78
82
  }
79
83
  }
@@ -1,4 +1,5 @@
1
- import { WRONG_MSG } from 'Constants/index';
1
+ import { WRONG_MSG } from '../constants/index';
2
+ import { ErrorsMsg } from '../types/general';
2
3
 
3
4
  export const prepareErrorMessage = (error: Error, defaultMsg?: string) => {
4
5
  if (typeof error === 'string') {
@@ -16,3 +17,30 @@ export const prepareErrorMessage = (error: Error, defaultMsg?: string) => {
16
17
  );
17
18
  }
18
19
  };
20
+
21
+ export const prepareErrorsArrayMessage = (errors: ErrorsMsg | null) => {
22
+ const msg: string[] = [];
23
+
24
+ if (errors) {
25
+ Object.entries(errors).forEach(([_, values]) => {
26
+ if (values?.length > 0) {
27
+ values.forEach((i) => {
28
+ msg.push(i);
29
+ });
30
+ }
31
+ });
32
+ }
33
+
34
+ return msg;
35
+ };
36
+
37
+ export function uuidv4() {
38
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
39
+ /[xy]/g,
40
+ function (c) {
41
+ const r = (Math.random() * 16) | 0;
42
+ const v = c == 'x' ? r : (r & 0x3) | 0x8;
43
+ return v.toString(16);
44
+ }
45
+ );
46
+ }
@@ -1,10 +1,11 @@
1
- import { MessageEventTypeEnum, PaymentMethodEnum } from 'Enums/general';
2
- import { OrderFormInputsType, AgreementsType } from 'Types/order';
1
+ import { MessageEventTypeEnum, PaymentMethodEnum } from '../enums/general';
2
+ import { AgreementsType } from '../types/order';
3
3
 
4
4
  type AgreementModelType = {
5
5
  paymentMethod: PaymentMethodEnum;
6
- redirectUrl: string;
7
- data: OrderFormInputsType;
6
+ redirectUrl?: string;
7
+ showIframe?: boolean;
8
+ phoneNumber?: string;
8
9
  generateSubscriberContact: boolean;
9
10
  language: string;
10
11
  merchantAgreementUrl: string;
@@ -14,7 +15,8 @@ type AgreementModelType = {
14
15
  export const prepareAgreementModel = ({
15
16
  paymentMethod,
16
17
  redirectUrl,
17
- data,
18
+ showIframe,
19
+ phoneNumber,
18
20
  generateSubscriberContact,
19
21
  language,
20
22
  merchantAgreementUrl,
@@ -23,11 +25,29 @@ export const prepareAgreementModel = ({
23
25
  const model: AgreementsType = {
24
26
  paymentProvider: paymentMethod,
25
27
  };
28
+ const existUrl = new URL(window.location.href);
29
+ const journey = existUrl.searchParams.get('journey');
30
+ const url = showIframe
31
+ ? window.location.href.split('?')[0]
32
+ : redirectUrl || window.location.href;
33
+ const cancelUrl = new URL(url);
34
+ cancelUrl.searchParams.append('action', `${MessageEventTypeEnum.CANCEL}`);
35
+ if (journey) {
36
+ cancelUrl.searchParams.append('journey', journey);
37
+ }
38
+ const confirmUrl = new URL(url);
39
+ confirmUrl.searchParams.append(
40
+ 'action',
41
+ `${MessageEventTypeEnum.COMPLETE}`
42
+ );
43
+ if (journey) {
44
+ confirmUrl.searchParams.append('journey', journey);
45
+ }
26
46
 
27
47
  if (paymentMethod === PaymentMethodEnum.SwedbankPay) {
28
48
  model.swedbankPay = {
29
- cancelUrl: `${redirectUrl}?action=${MessageEventTypeEnum.CANCEL}`,
30
- completeUrl: `${redirectUrl}?action=${MessageEventTypeEnum.COMPLETE}`,
49
+ cancelUrl: `${cancelUrl}`,
50
+ completeUrl: `${confirmUrl}`,
31
51
  language,
32
52
  accountId,
33
53
  };
@@ -41,8 +61,8 @@ export const prepareAgreementModel = ({
41
61
  generateSubscriberContact,
42
62
  merchantAgreementUrl,
43
63
  accountId,
44
- phoneNumber: data.phoneNumber,
45
- merchantRedirectUrl: redirectUrl,
64
+ phoneNumber,
65
+ merchantRedirectUrl: redirectUrl || window.location.href,
46
66
  },
47
67
  };
48
68
  };
package/tsconfig.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "downlevelIteration": true,
6
+ "allowJs": true,
7
+ "checkJs": false,
8
+ "skipLibCheck": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "strict": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "node",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": false,
18
+ "noEmit": false,
19
+ "noImplicitAny": true,
20
+ "jsx": "react-jsx",
21
+ "baseUrl": "src",
22
+ "sourceMap": true,
23
+ "inlineSources": true,
24
+ "sourceRoot": "/",
25
+ "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/*"]
37
+ }
38
+ },
39
+ "include": ["src"],
40
+ "exclude": ["node_modules"]
41
+ }