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

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.
Files changed (36) hide show
  1. package/.release-it.json +6 -0
  2. package/lib/package.json +42 -0
  3. package/lib/script.js +2 -0
  4. package/lib/script.js.LICENSE.txt +39 -0
  5. package/package.json +6 -5
  6. package/src/App.scss +2 -2
  7. package/src/App.tsx +97 -18
  8. package/src/api/OrdersApi.ts +15 -2
  9. package/src/api/SubscribeApi.ts +5 -1
  10. package/src/assets/styles/forms.scss +1 -1
  11. package/src/assets/styles/general.scss +2 -2
  12. package/src/components/Alert/alert.scss +1 -1
  13. package/src/components/Button/Button.tsx +1 -1
  14. package/src/components/Button/button.scss +1 -1
  15. package/src/components/FormFields/AddressField.tsx +52 -0
  16. package/src/components/FormFields/EmailField.tsx +5 -1
  17. package/src/components/FormFields/FormFields.helper.ts +5 -1
  18. package/src/components/FormFields/FormFieldsMapper.ts +9 -3
  19. package/src/components/FormFields/InputField.tsx +51 -0
  20. package/src/components/FormFields/NameField.tsx +5 -1
  21. package/src/components/FormFields/PhoneField.tsx +5 -1
  22. package/src/components/Loader/loader.scss +1 -1
  23. package/src/components/MainIframe/MainIframe.tsx +1 -1
  24. package/src/components/OrderForm/OrderForm.tsx +73 -17
  25. package/src/components/OrderForm/orderForm.scss +1 -1
  26. package/src/constants/index.ts +1 -1
  27. package/src/enums/general.ts +2 -0
  28. package/src/hooks/useMessageEvent.tsx +3 -2
  29. package/src/index.tsx +2 -2
  30. package/src/types/api.ts +1 -1
  31. package/src/types/general.ts +6 -1
  32. package/src/types/order.ts +6 -1
  33. package/src/utils/fetcher.ts +11 -7
  34. package/src/utils/helper.ts +29 -1
  35. package/src/utils/order.helper.ts +29 -9
  36. package/tsconfig.json +41 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @license React
3
+ * react-dom.production.min.js
4
+ *
5
+ * Copyright (c) Facebook, Inc. and its affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */
10
+
11
+ /**
12
+ * @license React
13
+ * react-jsx-runtime.production.min.js
14
+ *
15
+ * Copyright (c) Facebook, Inc. and its affiliates.
16
+ *
17
+ * This source code is licensed under the MIT license found in the
18
+ * LICENSE file in the root directory of this source tree.
19
+ */
20
+
21
+ /**
22
+ * @license React
23
+ * react.production.min.js
24
+ *
25
+ * Copyright (c) Facebook, Inc. and its affiliates.
26
+ *
27
+ * This source code is licensed under the MIT license found in the
28
+ * LICENSE file in the root directory of this source tree.
29
+ */
30
+
31
+ /**
32
+ * @license React
33
+ * scheduler.production.min.js
34
+ *
35
+ * Copyright (c) Facebook, Inc. and its affiliates.
36
+ *
37
+ * This source code is licensed under the MIT license found in the
38
+ * LICENSE file in the root directory of this source tree.
39
+ */
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-alpha1d",
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;