@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.
Files changed (40) hide show
  1. package/.prettierignore +1 -0
  2. package/.prettierrc.json +7 -0
  3. package/package.json +41 -0
  4. package/src/App.scss +24 -0
  5. package/src/App.tsx +184 -0
  6. package/src/ErrorBoundary.tsx +41 -0
  7. package/src/api/OrdersApi.ts +44 -0
  8. package/src/api/SubscribeApi.ts +51 -0
  9. package/src/assets/styles/forms.scss +105 -0
  10. package/src/assets/styles/general.scss +38 -0
  11. package/src/assets/styles/variables.scss +29 -0
  12. package/src/components/Alert/Alert.tsx +22 -0
  13. package/src/components/Alert/alert.scss +21 -0
  14. package/src/components/Button/Button.tsx +43 -0
  15. package/src/components/Button/button.scss +48 -0
  16. package/src/components/FormFields/EmailField.tsx +53 -0
  17. package/src/components/FormFields/FormFields.helper.ts +16 -0
  18. package/src/components/FormFields/FormFieldsMapper.ts +13 -0
  19. package/src/components/FormFields/NameField.tsx +46 -0
  20. package/src/components/FormFields/PhoneField.tsx +55 -0
  21. package/src/components/Loader/Loader.tsx +22 -0
  22. package/src/components/Loader/loader.scss +37 -0
  23. package/src/components/MainIframe/MainIframe.tsx +42 -0
  24. package/src/components/OrderForm/OrderForm.tsx +235 -0
  25. package/src/components/OrderForm/orderForm.scss +10 -0
  26. package/src/constants/index.ts +8 -0
  27. package/src/enums/api.ts +4 -0
  28. package/src/enums/general.ts +12 -0
  29. package/src/hooks/useIframe.tsx +59 -0
  30. package/src/hooks/useMessageEvent.tsx +142 -0
  31. package/src/hooks/useQueryParams.ts +5 -0
  32. package/src/index.tsx +22 -0
  33. package/src/types/api.ts +12 -0
  34. package/src/types/general.ts +50 -0
  35. package/src/types/order.ts +41 -0
  36. package/src/utils/fetcher.ts +89 -0
  37. package/src/utils/helper.ts +18 -0
  38. package/src/utils/order.helper.ts +48 -0
  39. package/tsconfig.json +41 -0
  40. package/webpack.config.js +103 -0
@@ -0,0 +1 @@
1
+ node_modules
@@ -0,0 +1,7 @@
1
+ {
2
+ "trailingComma": "es5",
3
+ "tabWidth": 4,
4
+ "semi": true,
5
+ "singleQuote": true,
6
+ "endOfLine": "auto"
7
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@infosoftas/s4-orders-js-sdk",
3
+ "version" : "0.0.1-alpha1",
4
+ "scripts": {
5
+ "build": "rimraf dist && webpack --env mode=production"
6
+ },
7
+ "eslintConfig": {
8
+ "extends": [
9
+ "react-app"
10
+ ]
11
+ },
12
+ "dependencies": {
13
+ "prop-types": "^15.8.1",
14
+ "react": "^18.3.1",
15
+ "react-dom": "^18.3.1",
16
+ "react-hook-form": "^7.53.0",
17
+ "rimraf": "^6.0.1"
18
+ },
19
+ "devDependencies": {
20
+ "@babel/core": "^7.24.3",
21
+ "@babel/preset-env": "^7.18.10",
22
+ "@babel/preset-react": "^7.18.6",
23
+ "@babel/preset-typescript": "^7.24.7",
24
+ "@types/react": "^18.0.17",
25
+ "@types/react-dom": "^18.0.6",
26
+ "babel-loader": "^8.3.0",
27
+ "css-loader": "^7.1.2",
28
+ "dotenv-webpack": "^8.1.0",
29
+ "mini-css-extract-plugin": "^2.9.1",
30
+ "sass": "^1.79.2",
31
+ "sass-loader": "^16.0.1",
32
+ "style-loader": "^4.0.0",
33
+ "ts-loader": "^9.5.1",
34
+ "typescript": "^5.6.2",
35
+ "webpack": "^5.95.0",
36
+ "webpack-cli": "^5.1.4"
37
+ },
38
+ "publishConfig": {
39
+ "access": "restricted"
40
+ }
41
+ }
package/src/App.scss ADDED
@@ -0,0 +1,24 @@
1
+ @import 'Assets/styles/variables.scss';
2
+ @import 'Assets/styles/general.scss';
3
+
4
+ .sdk-order-container {
5
+
6
+ *,
7
+ *::before,
8
+ *::after {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ .sdk-iframe-wrapper {
13
+ display : flex;
14
+ flex-direction: column;
15
+ align-items : center;
16
+ }
17
+
18
+ iframe {
19
+ max-width : 640px;
20
+ border : none;
21
+ background : transparent;
22
+ border-radius: $border-radius * 2;
23
+ }
24
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,184 @@
1
+ import { FC, useEffect, useState } from 'react';
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';
13
+
14
+ import ErrorBoundary from './ErrorBoundary';
15
+
16
+ import './App.scss';
17
+
18
+ const App: FC<ConfigType> = ({
19
+ moduleTitle,
20
+ apiKey,
21
+ templatePackageId,
22
+ subscriberId,
23
+ userId,
24
+ identityProviderId,
25
+ organizationId,
26
+ redirectUrl,
27
+ showIframe,
28
+ paymentMethodsOptions,
29
+ availablePaymentMethods = [],
30
+ language = 'en-US',
31
+ merchantAgreementUrl = '',
32
+ settings = {
33
+ successText: 'Order successful completed!',
34
+ failureText: 'Something went wrong!',
35
+ buttonText: 'Start',
36
+ },
37
+ }) => {
38
+ const [loading, setLoading] = useState<boolean>(false);
39
+ const [showOrderForm, setShowOrderForm] = useState<boolean>(true);
40
+ const [iframeSrc, setIframeSrc] = useState<string | null>(null);
41
+ const [orderId, setOrderId] = useState<string | null>(null);
42
+ const [isConfirmed, setIsConfirmed] = useState<boolean>(false);
43
+ const [isFailed, setIsFailed] = useState<boolean>(false);
44
+ const [failedMsg, setFailedMsg] = useState<string>('');
45
+ const [orderInfo, setOrderInfo] = useState<OrderInfoType | null>(null);
46
+
47
+ useEffect(() => {
48
+ if (apiKey) {
49
+ sessionStorage.setItem('sdk_api_key', apiKey);
50
+ }
51
+ }, [apiKey]);
52
+
53
+ const messageCallback = async (
54
+ data: CompleteOrderParamsType
55
+ ): Promise<void> => {
56
+ setLoading(true);
57
+ setIframeSrc(null);
58
+ setShowOrderForm(false);
59
+ setIsFailed(false);
60
+ try {
61
+ await orderComplete(orderId || data.orderId || '');
62
+ setIsConfirmed(true);
63
+ setLoading(false);
64
+ if (window === top) {
65
+ top.postMessage(
66
+ {
67
+ type: MessageEventTypeEnum.ORDER_FLOW_COMPLETE,
68
+ isCompleted: true,
69
+ orderInfo: orderInfo,
70
+ },
71
+ top?.location?.origin || '*'
72
+ );
73
+ }
74
+ } catch (error) {
75
+ console.log(error);
76
+ setFailedMsg(
77
+ prepareErrorMessage(error as Error, settings?.failureText)
78
+ );
79
+ setIsFailed(true);
80
+ setLoading(false);
81
+ setShowOrderForm(true);
82
+ }
83
+ };
84
+
85
+ const handleMessageEvent = (type: MessageEventTypeEnum) => {
86
+ if (type === MessageEventTypeEnum.CANCEL) {
87
+ setLoading(false);
88
+ setOrderId(null);
89
+ setOrderInfo(null);
90
+ setIframeSrc(null);
91
+ setIsConfirmed(false);
92
+ setIsFailed(false);
93
+ setShowOrderForm(true);
94
+ }
95
+ };
96
+
97
+ useMessageEvent(
98
+ messageCallback,
99
+ handleMessageEvent,
100
+ !!showIframe,
101
+ orderInfo
102
+ );
103
+
104
+ const handleForm = (url: string | null, data?: OrderInfoType | null) => {
105
+ setIsConfirmed(false);
106
+ setIsFailed(false);
107
+ setOrderId(data?.orderId || null);
108
+ setOrderInfo(data || null);
109
+ if (url) {
110
+ if (
111
+ showIframe &&
112
+ data?.paymentMethod === PaymentMethodEnum.SwedbankPay
113
+ ) {
114
+ setIframeSrc(url);
115
+ return;
116
+ }
117
+
118
+ window.location.href = url;
119
+ return;
120
+ }
121
+
122
+ setIframeSrc(null);
123
+ };
124
+
125
+ if (window !== top) {
126
+ return null;
127
+ }
128
+
129
+ return (
130
+ <ErrorBoundary>
131
+ <div
132
+ className={`sdk-order-container ${
133
+ iframeSrc ? 'show-iframe' : ''
134
+ }`}
135
+ data-testid="sdk-order-module-id"
136
+ >
137
+ {moduleTitle && <h1 className="text-center">{moduleTitle}</h1>}
138
+ {loading && (
139
+ <div className="d-flex justify-center">
140
+ <Loader size="lg" dark={true} />
141
+ </div>
142
+ )}
143
+ {!iframeSrc && showOrderForm && (
144
+ <OrderForm
145
+ callback={handleForm}
146
+ templatePackageId={templatePackageId}
147
+ subscriberId={subscriberId}
148
+ userId={userId}
149
+ identityProviderId={identityProviderId}
150
+ organizationId={organizationId}
151
+ paymentMethods={availablePaymentMethods}
152
+ buttonText={settings?.buttonText}
153
+ paymentMethodLabel={settings?.paymentMethodLabel}
154
+ errorReqMsg={settings?.errorReqMsg}
155
+ errorInvalidEmailMsg={settings?.errorInvalidEmailMsg}
156
+ errorInvalidPhoneMsg={settings?.errorInvalidPhoneMsg}
157
+ defaultValues={settings?.orderDefaultValues}
158
+ redirectUrl={
159
+ showIframe
160
+ ? window.location.href.split('?')[0]
161
+ : redirectUrl ||
162
+ window.location.href.split('?')[0]
163
+ }
164
+ paymentMethodsOptions={paymentMethodsOptions}
165
+ language={language}
166
+ merchantAgreementUrl={merchantAgreementUrl}
167
+ />
168
+ )}
169
+ {showIframe && <MainIframe iframeSrc={iframeSrc} />}
170
+ {isConfirmed && (
171
+ <Alert
172
+ className="mt-2"
173
+ type="success"
174
+ msg={settings?.successText}
175
+ />
176
+ )}
177
+
178
+ {isFailed && <Alert className="mt-2" msg={failedMsg} />}
179
+ </div>
180
+ </ErrorBoundary>
181
+ );
182
+ };
183
+
184
+ export default App;
@@ -0,0 +1,41 @@
1
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
2
+
3
+ interface Props {
4
+ children?: ReactNode;
5
+ }
6
+
7
+ interface State {
8
+ hasError: boolean;
9
+ }
10
+
11
+ class ErrorBoundary extends Component<Props, State> {
12
+ public state: State = {
13
+ hasError: false,
14
+ };
15
+
16
+ public static getDerivedStateFromError(): State {
17
+ // Update state so the next render will show the fallback UI.
18
+ return { hasError: true };
19
+ }
20
+
21
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
22
+ console.error('Uncaught error:', error, errorInfo);
23
+ }
24
+
25
+ public render() {
26
+ if (this.state.hasError) {
27
+ return (
28
+ <div className="layout-wrapper">
29
+ <h1>
30
+ Something went wrong. Please reload the page and try
31
+ again.
32
+ </h1>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ return this.props.children;
38
+ }
39
+ }
40
+
41
+ export default ErrorBoundary;
@@ -0,0 +1,44 @@
1
+ import { AgreementsType } from 'Types/order';
2
+ import fetcher from 'Utils/fetcher';
3
+
4
+ type RequestOrderStartType = {
5
+ subscriberId?: string;
6
+ subscriptionPlan?: {
7
+ organizationId?: string;
8
+ templateSubscriptionPlanId?: string;
9
+ };
10
+ paymentAgreement: AgreementsType;
11
+ };
12
+
13
+ type OrderResponseType = {
14
+ terminalRedirectUrl: string;
15
+ id: string;
16
+ };
17
+
18
+ export const orderStart = (
19
+ body: RequestOrderStartType
20
+ ): Promise<OrderResponseType> =>
21
+ fetcher<OrderResponseType, RequestOrderStartType>({
22
+ method: 'POST',
23
+ url: '/order',
24
+ headers: {
25
+ 'S4-ORDERS-API-KEY':
26
+ sessionStorage.getItem('sdk_api_key') ||
27
+ process.env.API_KEY ||
28
+ '',
29
+ },
30
+ body,
31
+ }) as Promise<OrderResponseType>;
32
+
33
+ export const orderComplete = (id: string): Promise<OrderResponseType> =>
34
+ fetcher<OrderResponseType, null>({
35
+ method: 'POST',
36
+ url: `/order/${id}/complete`,
37
+ headers: {
38
+ 'S4-ORDERS-API-KEY':
39
+ sessionStorage.getItem('sdk_api_key') ||
40
+ process.env.API_KEY ||
41
+ '',
42
+ },
43
+ body: null,
44
+ }) as Promise<OrderResponseType>;
@@ -0,0 +1,51 @@
1
+ import fetcher from 'Utils/fetcher';
2
+
3
+ type CreateSubscriberRequestType = {
4
+ name?: string;
5
+ email?: string;
6
+ phone?: string;
7
+ };
8
+
9
+ type CreateSubscriberResponseType = {
10
+ id: string;
11
+ };
12
+
13
+ type MapSubscriberToUserRequestType = {
14
+ identityProviderId: string;
15
+ userId: string;
16
+ };
17
+
18
+ type MapSubscriberToUserResponseType = {
19
+ id: string;
20
+ };
21
+
22
+ export const createSubscriber = (
23
+ body: CreateSubscriberRequestType
24
+ ): Promise<CreateSubscriberResponseType> =>
25
+ fetcher<CreateSubscriberResponseType, CreateSubscriberRequestType>({
26
+ method: 'POST',
27
+ url: '/subscriber',
28
+ headers: {
29
+ 'S4-ORDERS-API-KEY':
30
+ sessionStorage.getItem('sdk_api_key') ||
31
+ process.env.API_KEY ||
32
+ '',
33
+ },
34
+ body,
35
+ }) as Promise<CreateSubscriberResponseType>;
36
+
37
+ export const mapSubscriberToUser = (
38
+ id: string,
39
+ body: MapSubscriberToUserRequestType
40
+ ): Promise<MapSubscriberToUserResponseType> =>
41
+ fetcher<MapSubscriberToUserResponseType, MapSubscriberToUserRequestType>({
42
+ method: 'POST',
43
+ url: `/subscriber/${id}/user`,
44
+ headers: {
45
+ 'S4-ORDERS-API-KEY':
46
+ sessionStorage.getItem('sdk_api_key') ||
47
+ process.env.API_KEY ||
48
+ '',
49
+ },
50
+ body,
51
+ }) as Promise<MapSubscriberToUserResponseType>;
@@ -0,0 +1,105 @@
1
+ @import 'Assets/styles/variables.scss';
2
+
3
+ .field-label {
4
+ display : block;
5
+ margin-bottom: 0.375em;
6
+ font-weight : 500;
7
+ text-align : left;
8
+ }
9
+
10
+ .field-wrapper {
11
+ display : flex;
12
+ flex-direction: column;
13
+ width : 100%;
14
+ margin-bottom : 20px;
15
+
16
+ label {
17
+ display : block;
18
+ order : 1;
19
+ margin-bottom: 0.375em;
20
+ font-weight : 500;
21
+ text-align : left;
22
+ }
23
+
24
+ .caption {
25
+ order: 3;
26
+ }
27
+
28
+ .input-control {
29
+ order: 2;
30
+ }
31
+ }
32
+
33
+ .input-control {
34
+ width : 100%;
35
+ padding : 0.25em 0.5em;
36
+ border : 2px solid $input-border-color;
37
+ border-radius : 4px;
38
+ font-size : 16px;
39
+ font-size : max(16px, 1em);
40
+ font-family : inherit;
41
+ background-color: $white;
42
+
43
+ &[disabled],
44
+ &[readonly],
45
+ &:read-only,
46
+ .disabled,
47
+ .red-only {
48
+ border-color : $input-border-disabled-color;
49
+ background-color: $input-border-disabled-bg-color;
50
+ cursor : not-allowed;
51
+ }
52
+
53
+ &:focus {
54
+ border-color: hsl($input-focus-h, $input-focus-s, $input-focus-l);
55
+ }
56
+ }
57
+
58
+ .radio-button-control {
59
+ label {
60
+ display : flex;
61
+ margin-bottom: 0.375em;
62
+ font-weight : 500;
63
+ position : relative;
64
+ overflow : hidden;
65
+ cursor : pointer;
66
+
67
+ input {
68
+ position: absolute;
69
+ left : -9999px;
70
+
71
+ &:checked+span.value-block {
72
+ background-color: mix($white, $primary-color, 84%);
73
+
74
+ &:before {
75
+ box-shadow: inset 0 0 0 0.4375em $primary-color;
76
+ }
77
+ }
78
+ }
79
+
80
+ span.value-block {
81
+ display : flex;
82
+ align-items : center;
83
+ padding : 0.375em 0.75em 0.375em 0.375em;
84
+ border-radius: 20px;
85
+ transition : 0.25s ease;
86
+
87
+ &:hover {
88
+ background-color: mix($white, $primary-color, 84%);
89
+ }
90
+
91
+ &:before {
92
+ content : "";
93
+ display : flex;
94
+ flex-shrink : 0;
95
+ width : 1.5em;
96
+ height : 1.5em;
97
+ margin-right : 0.375em;
98
+ border-radius : 50%;
99
+ background-color: $white;
100
+ box-shadow : inset 0 0 0 0.125em $primary-color;
101
+ transition : 0.25s ease;
102
+ }
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,38 @@
1
+ @import 'Assets/styles/variables.scss';
2
+ @import 'Assets/styles/forms.scss';
3
+
4
+ .d-flex {
5
+ display: flex;
6
+ }
7
+
8
+ .justify-center {
9
+ justify-content: center;
10
+ }
11
+
12
+ .align-center {
13
+ align-items: center;
14
+ }
15
+
16
+ .mt-2 {
17
+ margin-top: 0.5rem;
18
+ }
19
+
20
+ .mb-2 {
21
+ margin-bottom: 0.5rem;
22
+ }
23
+
24
+ .text-error {
25
+ color: $error-color !important;
26
+ }
27
+
28
+ .text-success {
29
+ color: $success-color !important
30
+ }
31
+
32
+ .text-center {
33
+ text-align: center;
34
+ }
35
+
36
+ .caption {
37
+ font-size: $xs-font-size;
38
+ }
@@ -0,0 +1,29 @@
1
+ $white : #fff;
2
+ $black : #000;
3
+ $blue : #007bff;
4
+ $green : #5fa757;
5
+ $red : #c9252d;
6
+ $grey : #6c757d;
7
+ $light-gray : #f8f9fa;
8
+ $dark-gray : #343a40;
9
+ $error-color : $red;
10
+ $success-test-color : $green;
11
+ $text-color : $dark-gray;
12
+ $primary-color : $blue;
13
+ $input-border-color : $grey;
14
+ $input-border-disabled-color : #ccc;
15
+ $input-border-disabled-bg-color: #eee;
16
+ $success-color : #155724;
17
+ $success-bg-color : #d4edda;
18
+ $success-border-color : #c3e6cb;
19
+ $danger-color : #721c24;
20
+ $danger-bg-color : #f8d7da;
21
+ $danger-border-color : #f5c6cb;
22
+ $input-focus-h : 245;
23
+ $input-focus-s : 100%;
24
+ $input-focus-l : 42%;
25
+ $font-size : 1rem;
26
+ $sm-font-size : 0.875rem;
27
+ $xs-font-size : 0.75rem;
28
+ $border-radius : 4px;
29
+ $size-bezel : 0.5rem;
@@ -0,0 +1,22 @@
1
+ import { FC } from 'react';
2
+
3
+ import './alert.scss';
4
+
5
+ type Props = {
6
+ msg?: string;
7
+ type?: 'danger' | 'success';
8
+ className?: string;
9
+ };
10
+
11
+ const Alert: FC<Props> = ({ msg, type = 'danger', className = '' }) => {
12
+ return msg ? (
13
+ <div
14
+ className={`sdk-alert ${type} ${className}`}
15
+ data-testid="sdk-alert-id"
16
+ >
17
+ {msg}
18
+ </div>
19
+ ) : null;
20
+ };
21
+
22
+ export default Alert;
@@ -0,0 +1,21 @@
1
+ @import 'Assets/styles/variables.scss';
2
+
3
+ .sdk-alert {
4
+ position : relative;
5
+ padding : .75rem 1.25rem;
6
+ margin-bottom: 1rem;
7
+ border : 1px solid transparent;
8
+ border-radius: .25rem;
9
+
10
+ &.danger {
11
+ color : $danger-color;
12
+ background-color: $danger-bg-color;
13
+ border-color : $danger-border-color;
14
+ }
15
+
16
+ &.success {
17
+ color : $success-color;
18
+ background-color: $success-bg-color;
19
+ border-color : $success-border-color;
20
+ }
21
+ }
@@ -0,0 +1,43 @@
1
+ import { FC, MouseEvent } from 'react';
2
+
3
+ import Loader from 'Component/Loader/Loader';
4
+
5
+ import './button.scss';
6
+
7
+ type Props = {
8
+ onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
9
+ type?: 'button' | 'submit' | 'reset';
10
+ btnType?: 'primary';
11
+ buttonText?: string;
12
+ loading?: boolean;
13
+ disable?: boolean;
14
+ };
15
+
16
+ const Button: FC<Props> = ({
17
+ onClick,
18
+ type = 'button',
19
+ btnType = 'primary',
20
+ buttonText = 'Start',
21
+ loading = false,
22
+ disable = false,
23
+ }) => {
24
+ const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
25
+ onClick?.(e);
26
+ };
27
+
28
+ return (
29
+ <button
30
+ data-testid="sdk-button-id"
31
+ type={type}
32
+ disabled={loading || disable}
33
+ className={`sdk-btn ${btnType ? `${btnType}-btn` : ''} ${
34
+ loading ? 'loading' : ''
35
+ } ${disable ? 'disabled' : ''}`}
36
+ onClick={handleClick}
37
+ >
38
+ {loading ? <Loader className="btn-loader" /> : buttonText}
39
+ </button>
40
+ );
41
+ };
42
+
43
+ export default Button;