@infosoftas/s4-orders-js-sdk 0.0.1-alpha1
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/.prettierignore +1 -0
- package/.prettierrc.json +7 -0
- package/package.json +41 -0
- package/src/App.scss +24 -0
- package/src/App.tsx +184 -0
- package/src/ErrorBoundary.tsx +41 -0
- package/src/api/OrdersApi.ts +44 -0
- package/src/api/SubscribeApi.ts +51 -0
- package/src/assets/styles/forms.scss +105 -0
- package/src/assets/styles/general.scss +38 -0
- package/src/assets/styles/variables.scss +29 -0
- package/src/components/Alert/Alert.tsx +22 -0
- package/src/components/Alert/alert.scss +21 -0
- package/src/components/Button/Button.tsx +43 -0
- package/src/components/Button/button.scss +48 -0
- package/src/components/FormFields/EmailField.tsx +53 -0
- package/src/components/FormFields/FormFields.helper.ts +16 -0
- package/src/components/FormFields/FormFieldsMapper.ts +13 -0
- package/src/components/FormFields/NameField.tsx +46 -0
- package/src/components/FormFields/PhoneField.tsx +55 -0
- package/src/components/Loader/Loader.tsx +22 -0
- package/src/components/Loader/loader.scss +37 -0
- package/src/components/MainIframe/MainIframe.tsx +42 -0
- package/src/components/OrderForm/OrderForm.tsx +235 -0
- package/src/components/OrderForm/orderForm.scss +10 -0
- package/src/constants/index.ts +8 -0
- package/src/enums/api.ts +4 -0
- package/src/enums/general.ts +12 -0
- package/src/hooks/useIframe.tsx +59 -0
- package/src/hooks/useMessageEvent.tsx +142 -0
- package/src/hooks/useQueryParams.ts +5 -0
- package/src/index.tsx +22 -0
- package/src/types/api.ts +12 -0
- package/src/types/general.ts +50 -0
- package/src/types/order.ts +41 -0
- package/src/utils/fetcher.ts +89 -0
- package/src/utils/helper.ts +18 -0
- package/src/utils/order.helper.ts +48 -0
- package/tsconfig.json +41 -0
- package/webpack.config.js +103 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@use "sass:color";
|
|
2
|
+
|
|
3
|
+
@import 'Assets/styles/variables.scss';
|
|
4
|
+
|
|
5
|
+
.sdk-btn {
|
|
6
|
+
appearance : button;
|
|
7
|
+
-webkit-appearance : button;
|
|
8
|
+
display : inline-block;
|
|
9
|
+
padding : 6px 12px;
|
|
10
|
+
margin-bottom : 0;
|
|
11
|
+
border : 1px solid $input-border-color;
|
|
12
|
+
border-radius : $border-radius;
|
|
13
|
+
white-space : nowrap;
|
|
14
|
+
vertical-align : middle;
|
|
15
|
+
font-size : $sm-font-size;
|
|
16
|
+
line-height : 1.42857143;
|
|
17
|
+
font-weight : 400;
|
|
18
|
+
text-align : center;
|
|
19
|
+
background-image : none;
|
|
20
|
+
background-color : $white;
|
|
21
|
+
color : $text-color;
|
|
22
|
+
cursor : pointer;
|
|
23
|
+
-webkit-user-select: none;
|
|
24
|
+
-moz-user-select : none;
|
|
25
|
+
-ms-user-select : none;
|
|
26
|
+
user-select : none;
|
|
27
|
+
-ms-touch-action : manipulation;
|
|
28
|
+
touch-action : manipulation;
|
|
29
|
+
|
|
30
|
+
&.loading,
|
|
31
|
+
&.disabled,
|
|
32
|
+
&:disabled {
|
|
33
|
+
opacity : 0.75;
|
|
34
|
+
pointer-events: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&.primary-btn {
|
|
38
|
+
color : $white;
|
|
39
|
+
background-color: $primary-color;
|
|
40
|
+
border-color : color.scale($primary-color, $lightness: -20%);
|
|
41
|
+
|
|
42
|
+
&:hover,
|
|
43
|
+
&:focus {
|
|
44
|
+
background-color: color.scale($primary-color, $lightness: -10%);
|
|
45
|
+
border-color : color.scale($primary-color, $lightness: -30%);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { useFormContext } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
name: string;
|
|
6
|
+
required: boolean;
|
|
7
|
+
readOnly: boolean;
|
|
8
|
+
label?: string;
|
|
9
|
+
errorReqMsg?: string;
|
|
10
|
+
errorInvalidEmailMsg?: string;
|
|
11
|
+
errors?: {
|
|
12
|
+
email?: {
|
|
13
|
+
message?: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const EmailField: FC<Props> = ({
|
|
19
|
+
required = false,
|
|
20
|
+
readOnly = false,
|
|
21
|
+
label = 'Email',
|
|
22
|
+
errorReqMsg = 'This field is required!',
|
|
23
|
+
errorInvalidEmailMsg = 'Invalid email address!',
|
|
24
|
+
errors,
|
|
25
|
+
}) => {
|
|
26
|
+
const { register } = useFormContext();
|
|
27
|
+
return (
|
|
28
|
+
<div className="field-wrapper" data-testid="sdk-email-field-id">
|
|
29
|
+
<input
|
|
30
|
+
autoComplete="email"
|
|
31
|
+
placeholder=" "
|
|
32
|
+
className={`input-control ${readOnly ? 'read-only' : ''}`}
|
|
33
|
+
readOnly={readOnly}
|
|
34
|
+
type="email"
|
|
35
|
+
{...register('email', {
|
|
36
|
+
required: required ? errorReqMsg : false,
|
|
37
|
+
pattern: {
|
|
38
|
+
value: /^[-!#-'*+/-9=?^-~]+(?:\.[-!#-'*+/-9=?^-~]+)*@[-!#-'*+/-9=?^-~]+(?:\.[-!#-'*+/-9=?^-~]{2,20})+$/i,
|
|
39
|
+
message: errorInvalidEmailMsg,
|
|
40
|
+
},
|
|
41
|
+
})}
|
|
42
|
+
/>
|
|
43
|
+
<label className="label-control">
|
|
44
|
+
{label} {required && <span className="text-error">*</span>}
|
|
45
|
+
</label>
|
|
46
|
+
{errors && errors.email && (
|
|
47
|
+
<div className="text-error caption">{errors.email.message}</div>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default EmailField;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { OrderFormFiledType } from 'Types/general';
|
|
2
|
+
|
|
3
|
+
export const FIELD_TYPES = {
|
|
4
|
+
PHONE: 'phoneNumber',
|
|
5
|
+
EMAIL: 'email',
|
|
6
|
+
NAME: 'name',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_ORDER_FORM_FIELDS: OrderFormFiledType[] = [
|
|
10
|
+
{
|
|
11
|
+
name: FIELD_TYPES.PHONE,
|
|
12
|
+
required: false,
|
|
13
|
+
readOnly: false,
|
|
14
|
+
label: '',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import EmailField from 'Component/FormFields/EmailField';
|
|
2
|
+
import PhoneField from 'Component/FormFields/PhoneField';
|
|
3
|
+
import NameField from 'Component/FormFields/NameField';
|
|
4
|
+
|
|
5
|
+
import { FIELD_TYPES } from './FormFields.helper';
|
|
6
|
+
|
|
7
|
+
const formFieldsMapper = {
|
|
8
|
+
[FIELD_TYPES.PHONE]: PhoneField,
|
|
9
|
+
[FIELD_TYPES.NAME]: NameField,
|
|
10
|
+
[FIELD_TYPES.EMAIL]: EmailField,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default formFieldsMapper;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { useFormContext } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
name: string;
|
|
6
|
+
required: boolean;
|
|
7
|
+
readOnly: boolean;
|
|
8
|
+
label?: string;
|
|
9
|
+
errorReqMsg?: string;
|
|
10
|
+
errors?: {
|
|
11
|
+
name?: {
|
|
12
|
+
message?: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const NameField: FC<Props> = ({
|
|
18
|
+
required = false,
|
|
19
|
+
readOnly = false,
|
|
20
|
+
label = 'Name',
|
|
21
|
+
errorReqMsg = 'This field is required!',
|
|
22
|
+
errors,
|
|
23
|
+
}) => {
|
|
24
|
+
const { register } = useFormContext();
|
|
25
|
+
return (
|
|
26
|
+
<div className="field-wrapper" data-testid="sdk-name-field-id">
|
|
27
|
+
<input
|
|
28
|
+
autoComplete="name"
|
|
29
|
+
placeholder=" "
|
|
30
|
+
readOnly={readOnly}
|
|
31
|
+
className={`input-control ${readOnly ? 'read-only' : ''}`}
|
|
32
|
+
{...register('name', {
|
|
33
|
+
required: required ? errorReqMsg : false,
|
|
34
|
+
})}
|
|
35
|
+
/>
|
|
36
|
+
<label className="label-control">
|
|
37
|
+
{label} {required && <span className="text-error">*</span>}
|
|
38
|
+
</label>
|
|
39
|
+
{errors && errors.name && (
|
|
40
|
+
<div className="text-error caption">{errors.name.message}</div>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default NameField;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { useFormContext } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
name: string;
|
|
6
|
+
required: boolean;
|
|
7
|
+
readOnly: boolean;
|
|
8
|
+
label?: string;
|
|
9
|
+
errorReqMsg?: string;
|
|
10
|
+
errorInvalidPhoneMsg?: string;
|
|
11
|
+
errors?: {
|
|
12
|
+
phoneNumber?: {
|
|
13
|
+
message?: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const PhoneFiled: FC<Props> = ({
|
|
19
|
+
required = false,
|
|
20
|
+
readOnly = false,
|
|
21
|
+
label = 'Phone number',
|
|
22
|
+
errorReqMsg = 'This field is required!',
|
|
23
|
+
errorInvalidPhoneMsg = 'Invalid phone address!',
|
|
24
|
+
errors,
|
|
25
|
+
}) => {
|
|
26
|
+
const { register } = useFormContext();
|
|
27
|
+
return (
|
|
28
|
+
<div className="field-wrapper" data-testid="sdk-phone-field-id">
|
|
29
|
+
<input
|
|
30
|
+
autoComplete="tel"
|
|
31
|
+
className={`input-control ${readOnly ? 'read-only' : ''}`}
|
|
32
|
+
placeholder=" "
|
|
33
|
+
readOnly={readOnly}
|
|
34
|
+
type="tel"
|
|
35
|
+
{...register('phoneNumber', {
|
|
36
|
+
required: required ? errorReqMsg : false,
|
|
37
|
+
pattern: {
|
|
38
|
+
value: /^[+]*[(]{0,1}[0-9]{1,3}[)]{0,1}[-\s\./0-9]{6,14}$/g,
|
|
39
|
+
message: errorInvalidPhoneMsg,
|
|
40
|
+
},
|
|
41
|
+
})}
|
|
42
|
+
/>
|
|
43
|
+
<label className="label-control">
|
|
44
|
+
{label} {required && <span className="text-error">*</span>}
|
|
45
|
+
</label>
|
|
46
|
+
{errors && errors.phoneNumber && (
|
|
47
|
+
<div className="text-error caption">
|
|
48
|
+
{errors.phoneNumber.message}
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default PhoneFiled;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
import './loader.scss';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
className?: string;
|
|
7
|
+
size?: 'sm' | 'md' | 'lg';
|
|
8
|
+
dark?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const Loader: FC<Props> = ({ className = '', size = 'sm', dark = false }) => {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-testid="sdk-loader-id"
|
|
15
|
+
className={`sdk-loader ${className} size-${size} ${
|
|
16
|
+
dark ? 'dark-color' : ''
|
|
17
|
+
}`}
|
|
18
|
+
></div>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default Loader;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@import 'Assets/styles/variables.scss';
|
|
2
|
+
|
|
3
|
+
.sdk-loader {
|
|
4
|
+
border : 2px solid $light-gray;
|
|
5
|
+
border-top : 2px solid transparent;
|
|
6
|
+
border-radius: 50%;
|
|
7
|
+
width : 16px;
|
|
8
|
+
height : 16px;
|
|
9
|
+
animation : spin 2s linear infinite;
|
|
10
|
+
|
|
11
|
+
&.size-md {
|
|
12
|
+
border-width: 4px;
|
|
13
|
+
width : 32px;
|
|
14
|
+
height : 32px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
&.size-lg {
|
|
18
|
+
border-width: 8px;
|
|
19
|
+
width : 64px;
|
|
20
|
+
height : 64px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&.dark-color {
|
|
24
|
+
border-color : $dark-gray;
|
|
25
|
+
border-top-color: transparent;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@keyframes spin {
|
|
30
|
+
0% {
|
|
31
|
+
transform: rotate(0deg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
100% {
|
|
35
|
+
transform: rotate(360deg);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FC, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import useIframe from 'Hooks/useIframe';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
iframeSrc: string | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const MainIframe: FC<Props> = ({ iframeSrc }) => {
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
|
|
12
|
+
const { frameRef, messageReceiver } = useIframe();
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (frameRef && iframeSrc) {
|
|
16
|
+
setLoading(true);
|
|
17
|
+
frameRef?.current &&
|
|
18
|
+
(frameRef?.current as HTMLIFrameElement).addEventListener(
|
|
19
|
+
'load',
|
|
20
|
+
() => {
|
|
21
|
+
setLoading(false);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
messageReceiver();
|
|
25
|
+
}
|
|
26
|
+
}, [frameRef, iframeSrc]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="sdk-iframe-wrapper" data-testid="sdk-iframe-id">
|
|
30
|
+
{loading && <div>... Loading</div>}
|
|
31
|
+
{iframeSrc && (
|
|
32
|
+
<iframe
|
|
33
|
+
ref={frameRef}
|
|
34
|
+
src={iframeSrc}
|
|
35
|
+
style={{ minHeight: '830px', width: '100%' }}
|
|
36
|
+
></iframe>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default MainIframe;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { FC, ChangeEvent, Suspense, useState } from 'react';
|
|
2
|
+
import { FormProvider, useForm, SubmitHandler } from 'react-hook-form';
|
|
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';
|
|
16
|
+
|
|
17
|
+
import './orderForm.scss';
|
|
18
|
+
|
|
19
|
+
type Props = {
|
|
20
|
+
callback: (url: string | null, orderInfo?: OrderInfoType | null) => void;
|
|
21
|
+
templatePackageId: string;
|
|
22
|
+
subscriberId?: string;
|
|
23
|
+
userId?: string;
|
|
24
|
+
identityProviderId?: string;
|
|
25
|
+
organizationId: string;
|
|
26
|
+
redirectUrl: string;
|
|
27
|
+
language: string;
|
|
28
|
+
merchantAgreementUrl: string;
|
|
29
|
+
paymentMethodsOptions?: PaymentMethodOptionsType;
|
|
30
|
+
defaultValues?: OrderFormInputsType;
|
|
31
|
+
paymentMethods?: { label: string; value: PaymentMethodEnum }[];
|
|
32
|
+
buttonText?: string;
|
|
33
|
+
paymentMethodLabel?: string;
|
|
34
|
+
errorReqMsg?: string;
|
|
35
|
+
errorInvalidEmailMsg?: string;
|
|
36
|
+
errorInvalidPhoneMsg?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const initialData = {
|
|
40
|
+
name: '',
|
|
41
|
+
email: '',
|
|
42
|
+
phoneNumber: '',
|
|
43
|
+
paymentMethod: PAYMENT_METHOD_DEFAULT,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const OrderForm: FC<Props> = ({
|
|
47
|
+
callback,
|
|
48
|
+
templatePackageId,
|
|
49
|
+
subscriberId,
|
|
50
|
+
userId,
|
|
51
|
+
identityProviderId,
|
|
52
|
+
organizationId,
|
|
53
|
+
redirectUrl,
|
|
54
|
+
paymentMethodsOptions,
|
|
55
|
+
defaultValues,
|
|
56
|
+
language,
|
|
57
|
+
merchantAgreementUrl,
|
|
58
|
+
paymentMethods = [],
|
|
59
|
+
buttonText = 'Start',
|
|
60
|
+
paymentMethodLabel = 'Select Payment Method',
|
|
61
|
+
errorReqMsg = '',
|
|
62
|
+
errorInvalidEmailMsg = '',
|
|
63
|
+
errorInvalidPhoneMsg = '',
|
|
64
|
+
}) => {
|
|
65
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
66
|
+
const [apiErrorMsg, setApiErrorMsg] = useState<string>('');
|
|
67
|
+
const [orderFields, setOrderFields] = useState<OrderFormFiledType[]>(
|
|
68
|
+
paymentMethodsOptions?.[PAYMENT_METHOD_DEFAULT]?.orderFormFields ??
|
|
69
|
+
DEFAULT_ORDER_FORM_FIELDS
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const methods = useForm<OrderFormInputsType>({
|
|
73
|
+
defaultValues: {
|
|
74
|
+
...initialData,
|
|
75
|
+
...(defaultValues ?? {}),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const {
|
|
80
|
+
register,
|
|
81
|
+
handleSubmit,
|
|
82
|
+
setValue,
|
|
83
|
+
formState: { errors },
|
|
84
|
+
} = methods;
|
|
85
|
+
|
|
86
|
+
const onSubmit: SubmitHandler<OrderFormInputsType> = async (
|
|
87
|
+
data
|
|
88
|
+
): Promise<void> => {
|
|
89
|
+
setApiErrorMsg('');
|
|
90
|
+
|
|
91
|
+
setLoading(true);
|
|
92
|
+
try {
|
|
93
|
+
let id: string | undefined = subscriberId;
|
|
94
|
+
if (!subscriberId) {
|
|
95
|
+
const response = await createSubscriber({
|
|
96
|
+
name: data.name,
|
|
97
|
+
email: data.email,
|
|
98
|
+
phone: data.phoneNumber,
|
|
99
|
+
});
|
|
100
|
+
id = response.id;
|
|
101
|
+
|
|
102
|
+
if (userId && identityProviderId) {
|
|
103
|
+
await mapSubscriberToUser(id, {
|
|
104
|
+
userId: userId,
|
|
105
|
+
identityProviderId: identityProviderId,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (id) {
|
|
111
|
+
const paymentMethod =
|
|
112
|
+
data.paymentMethod || PAYMENT_METHOD_DEFAULT;
|
|
113
|
+
|
|
114
|
+
const agreements = prepareAgreementModel({
|
|
115
|
+
paymentMethod,
|
|
116
|
+
redirectUrl,
|
|
117
|
+
data,
|
|
118
|
+
language,
|
|
119
|
+
merchantAgreementUrl,
|
|
120
|
+
generateSubscriberContact:
|
|
121
|
+
paymentMethodsOptions?.[
|
|
122
|
+
data.paymentMethod as PaymentMethodEnum
|
|
123
|
+
]?.generateSubscriberContact || false,
|
|
124
|
+
accountId:
|
|
125
|
+
paymentMethodsOptions?.[
|
|
126
|
+
data.paymentMethod as PaymentMethodEnum
|
|
127
|
+
]?.accountId,
|
|
128
|
+
});
|
|
129
|
+
const responseOrder = await orderStart({
|
|
130
|
+
subscriberId: id,
|
|
131
|
+
subscriptionPlan: {
|
|
132
|
+
organizationId,
|
|
133
|
+
templateSubscriptionPlanId: templatePackageId,
|
|
134
|
+
},
|
|
135
|
+
paymentAgreement: { ...agreements },
|
|
136
|
+
});
|
|
137
|
+
callback(responseOrder.terminalRedirectUrl, {
|
|
138
|
+
orderId: responseOrder.id,
|
|
139
|
+
subscriberId: id,
|
|
140
|
+
paymentMethod: paymentMethod,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
callback(null);
|
|
145
|
+
setApiErrorMsg(WRONG_MSG);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(error);
|
|
148
|
+
setApiErrorMsg(prepareErrorMessage(error as Error));
|
|
149
|
+
callback(null);
|
|
150
|
+
} finally {
|
|
151
|
+
setLoading(false);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const handlePaymentChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
156
|
+
setValue('paymentMethod', e.target.value as PaymentMethodEnum);
|
|
157
|
+
const orderFieldsLocal =
|
|
158
|
+
paymentMethodsOptions?.[e.target.value as PaymentMethodEnum]
|
|
159
|
+
?.orderFormFields ?? DEFAULT_ORDER_FORM_FIELDS;
|
|
160
|
+
setOrderFields(orderFieldsLocal);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<FormProvider {...methods}>
|
|
165
|
+
<form
|
|
166
|
+
className="sdk-order-form"
|
|
167
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
168
|
+
data-testid="sdk-order-form-id"
|
|
169
|
+
>
|
|
170
|
+
{paymentMethods?.length > 0 && (
|
|
171
|
+
<div className="field-wrapper radio-button-group">
|
|
172
|
+
<div className="field-label">{paymentMethodLabel}</div>
|
|
173
|
+
{paymentMethods.map((item) => (
|
|
174
|
+
<div
|
|
175
|
+
className="radio-button-control"
|
|
176
|
+
key={item.value}
|
|
177
|
+
>
|
|
178
|
+
<label className="radio-badge">
|
|
179
|
+
<input
|
|
180
|
+
{...register('paymentMethod', {
|
|
181
|
+
required: true,
|
|
182
|
+
})}
|
|
183
|
+
onChange={handlePaymentChange}
|
|
184
|
+
type="radio"
|
|
185
|
+
value={item.value}
|
|
186
|
+
/>
|
|
187
|
+
<span className="value-block">
|
|
188
|
+
<span className="prefix-icon"></span>
|
|
189
|
+
<span className="text-block">
|
|
190
|
+
{item.label}
|
|
191
|
+
</span>
|
|
192
|
+
</span>
|
|
193
|
+
</label>
|
|
194
|
+
</div>
|
|
195
|
+
))}
|
|
196
|
+
{errors.paymentMethod && (
|
|
197
|
+
<div className="text-error caption">
|
|
198
|
+
{errors.paymentMethod.message}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
{orderFields?.length > 0 && (
|
|
204
|
+
<Suspense fallback={null}>
|
|
205
|
+
{orderFields.map((field) => {
|
|
206
|
+
const Component = formFieldsMapper[field.name];
|
|
207
|
+
|
|
208
|
+
return Component ? (
|
|
209
|
+
<Component
|
|
210
|
+
key={field.name}
|
|
211
|
+
name={field.name}
|
|
212
|
+
label={field.label}
|
|
213
|
+
required={field.required || false}
|
|
214
|
+
readOnly={field.readOnly || false}
|
|
215
|
+
errors={errors}
|
|
216
|
+
errorReqMsg={errorReqMsg}
|
|
217
|
+
errorInvalidEmailMsg={errorInvalidEmailMsg}
|
|
218
|
+
errorInvalidPhoneMsg={errorInvalidPhoneMsg}
|
|
219
|
+
/>
|
|
220
|
+
) : null;
|
|
221
|
+
})}
|
|
222
|
+
</Suspense>
|
|
223
|
+
)}
|
|
224
|
+
<Button
|
|
225
|
+
type="submit"
|
|
226
|
+
loading={loading}
|
|
227
|
+
buttonText={buttonText}
|
|
228
|
+
/>
|
|
229
|
+
<Alert className="mt-2" msg={apiErrorMsg} />
|
|
230
|
+
</form>
|
|
231
|
+
</FormProvider>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export default OrderForm;
|
package/src/enums/api.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export enum PaymentMethodEnum {
|
|
2
|
+
Vipps = 'Vipps',
|
|
3
|
+
MobilePay = 'MobilePay',
|
|
4
|
+
SwedbankPay = 'SwedbankPay',
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export enum MessageEventTypeEnum {
|
|
8
|
+
ORDER_FLOW_COMPLETE = 'orderFlowComplete',
|
|
9
|
+
VIPPS_COMPLETE = 'completeOrder',
|
|
10
|
+
CANCEL = 'cancel',
|
|
11
|
+
COMPLETE = 'complete',
|
|
12
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
let currentMessageReceiver: (event: Event) => void;
|
|
4
|
+
|
|
5
|
+
const useIframe = () => {
|
|
6
|
+
const frameRef = useRef(null);
|
|
7
|
+
|
|
8
|
+
const messageReceiver = (
|
|
9
|
+
callback?: (type: string, value: string) => void
|
|
10
|
+
) => {
|
|
11
|
+
if (currentMessageReceiver) {
|
|
12
|
+
window.removeEventListener('message', currentMessageReceiver);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
currentMessageReceiver = (event: Event) => {
|
|
16
|
+
if (!frameRef.current) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const childWindow = (frameRef.current as HTMLIFrameElement)
|
|
21
|
+
.contentWindow;
|
|
22
|
+
|
|
23
|
+
if ((event as MessageEvent).source !== childWindow) {
|
|
24
|
+
return; // Skip message in this event listener
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let { value, type } = (event as MessageEvent).data as unknown as {
|
|
28
|
+
value: string;
|
|
29
|
+
type: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (callback) {
|
|
33
|
+
callback(type, value);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
window.addEventListener('message', currentMessageReceiver, false);
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
removeMessageReceiver();
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const removeMessageReceiver = () => {
|
|
45
|
+
if (currentMessageReceiver) {
|
|
46
|
+
window.removeEventListener(
|
|
47
|
+
'addEventListener',
|
|
48
|
+
currentMessageReceiver
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
frameRef,
|
|
55
|
+
messageReceiver,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default useIframe;
|