@akinon/pz-haso 1.112.0-snapshot-ZERO-3859-20251125183557
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/.prettierrc +6 -0
- package/CHANGELOG.md +17 -0
- package/README.md +247 -0
- package/package.json +26 -0
- package/src/components/FormComponent.tsx +65 -0
- package/src/index.tsx +7 -0
- package/src/pages/HasoPaymentGateway.tsx +77 -0
- package/src/pages/api/check-availability.ts +107 -0
- package/src/redux/api.ts +39 -0
- package/src/types.ts +35 -0
- package/src/utils/index.ts +19 -0
package/.prettierrc
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# HASO Payment Gateway Extension
|
|
2
|
+
|
|
3
|
+
## 1.112.0-snapshot-ZERO-3859-20251125183557
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- cbcfdf4: ZERO-3859: Haso payment gateway implmeneted
|
|
8
|
+
|
|
9
|
+
## 1.0.0
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- Initial release of HASO (Hemen Al Sonra Öde) payment gateway extension
|
|
14
|
+
- Data aggregation support for redirect-based payment flow
|
|
15
|
+
- SHA-512 hash validation for secure data transfer
|
|
16
|
+
- Customizable renderer props for UI customization
|
|
17
|
+
- Collects order items, billing/shipping addresses, and customer info
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# HASO Payment Gateway Extension
|
|
2
|
+
|
|
3
|
+
HASO (Hemen Al Sonra Öde / Buy Now Pay Later) ödeme entegrasyonu için data aggregation paketi.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
You can use the following command to install the extension with the latest plugins:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @akinon/projectzero@latest --plugins
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Once the extension is installed, you can easily integrate the HASO payment gateway into your application. Here's an example of how to use it.
|
|
16
|
+
|
|
17
|
+
1. Navigate to the `src/app/[commerce]/[locale]/[currency]/payment-gateway/haso/` directory.
|
|
18
|
+
|
|
19
|
+
2. Create a file named `page.tsx` and include the following code:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { HasoPaymentGateway } from '@akinon/pz-haso';
|
|
23
|
+
|
|
24
|
+
const HasoGateway = async ({
|
|
25
|
+
searchParams: { sessionId },
|
|
26
|
+
params: { currency, locale }
|
|
27
|
+
}: {
|
|
28
|
+
searchParams: Record<string, string>;
|
|
29
|
+
params: { currency: string; locale: string };
|
|
30
|
+
}) => {
|
|
31
|
+
return (
|
|
32
|
+
<HasoPaymentGateway
|
|
33
|
+
sessionId={sessionId}
|
|
34
|
+
currency={currency}
|
|
35
|
+
locale={locale}
|
|
36
|
+
extensionUrl={process.env.HASO_EXTENSION_URL}
|
|
37
|
+
hashKey={process.env.HASO_HASH_KEY}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default HasoGateway;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Customizing the HASO Component
|
|
46
|
+
|
|
47
|
+
You can customize the appearance of the HASO payment gateway using the `renderer` prop. This allows you to provide custom rendering functions for different parts of the component.
|
|
48
|
+
|
|
49
|
+
### Custom Form Component
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { HasoPaymentGateway } from '@akinon/pz-haso';
|
|
53
|
+
|
|
54
|
+
const HasoGateway = async ({
|
|
55
|
+
searchParams: { sessionId },
|
|
56
|
+
params: { currency, locale }
|
|
57
|
+
}: {
|
|
58
|
+
searchParams: Record<string, string>;
|
|
59
|
+
params: { currency: string; locale: string };
|
|
60
|
+
}) => {
|
|
61
|
+
return (
|
|
62
|
+
<HasoPaymentGateway
|
|
63
|
+
sessionId={sessionId}
|
|
64
|
+
currency={currency}
|
|
65
|
+
locale={locale}
|
|
66
|
+
extensionUrl={process.env.HASO_EXTENSION_URL}
|
|
67
|
+
hashKey={process.env.HASO_HASH_KEY}
|
|
68
|
+
renderer={{
|
|
69
|
+
formComponent: {
|
|
70
|
+
renderForm: ({
|
|
71
|
+
extensionUrl,
|
|
72
|
+
sessionId,
|
|
73
|
+
context,
|
|
74
|
+
csrfToken,
|
|
75
|
+
autoSubmit
|
|
76
|
+
}) => (
|
|
77
|
+
<div className="custom-haso-form-wrapper">
|
|
78
|
+
<h3 className="text-lg font-semibold mb-4">HASO Ödeme</h3>
|
|
79
|
+
<p className="mb-4">
|
|
80
|
+
HASO ödeme sayfasına yönlendiriliyorsunuz...
|
|
81
|
+
</p>
|
|
82
|
+
<form
|
|
83
|
+
action={`${extensionUrl}/form-page/?sessionId=${sessionId}`}
|
|
84
|
+
method="post"
|
|
85
|
+
encType="multipart/form-data"
|
|
86
|
+
id="haso-custom-form"
|
|
87
|
+
className="hidden"
|
|
88
|
+
>
|
|
89
|
+
<input type="hidden" name="csrf_token" value={csrfToken} />
|
|
90
|
+
<input
|
|
91
|
+
type="hidden"
|
|
92
|
+
name="data"
|
|
93
|
+
value={JSON.stringify(context)}
|
|
94
|
+
/>
|
|
95
|
+
{autoSubmit && (
|
|
96
|
+
<script
|
|
97
|
+
dangerouslySetInnerHTML={{
|
|
98
|
+
__html:
|
|
99
|
+
"document.getElementById('haso-custom-form').submit()"
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
</form>
|
|
104
|
+
<div className="loader w-12 h-12 border-4 border-t-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
},
|
|
108
|
+
paymentGateway: {
|
|
109
|
+
renderContainer: ({ children }) => (
|
|
110
|
+
<div className="p-8 max-w-lg mx-auto bg-white rounded-lg shadow-md">
|
|
111
|
+
{children}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export default HasoGateway;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Custom Renderer API
|
|
124
|
+
|
|
125
|
+
The renderer prop accepts an object with the following structure:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
interface HasoRendererProps {
|
|
129
|
+
formComponent?: {
|
|
130
|
+
renderForm?: (props: {
|
|
131
|
+
extensionUrl: string;
|
|
132
|
+
sessionId: string;
|
|
133
|
+
context: any;
|
|
134
|
+
csrfToken: string;
|
|
135
|
+
autoSubmit: boolean;
|
|
136
|
+
}) => React.ReactNode;
|
|
137
|
+
renderLoading?: () => React.ReactNode;
|
|
138
|
+
renderError?: (error: string) => React.ReactNode;
|
|
139
|
+
};
|
|
140
|
+
paymentGateway?: {
|
|
141
|
+
renderContainer?: (props: { children: React.ReactNode }) => React.ReactNode;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Configuration
|
|
147
|
+
|
|
148
|
+
Add these variables to your `.env` file:
|
|
149
|
+
|
|
150
|
+
```env
|
|
151
|
+
HASO_EXTENSION_URL=<your_extension_url>
|
|
152
|
+
HASO_HASH_KEY=<your_hash_key>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Data Aggregation Flow
|
|
156
|
+
|
|
157
|
+
This package implements the data aggregation pattern for HASO payments:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
1. start-session → redirectUrl: /payment-gateway/haso/?sessionId=XXX
|
|
161
|
+
2. HasoPaymentGateway → Collects data (preOrder, addresses, basket)
|
|
162
|
+
3. FormComponent → POST → Extension /form-page/?sessionId=XXX (with SHA-512 hash)
|
|
163
|
+
4. Extension → Creates Provider URL → Payment screen
|
|
164
|
+
5. Payment completed → return-url → Merchant Store
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Data Collected
|
|
168
|
+
|
|
169
|
+
The package collects the following data from the checkout:
|
|
170
|
+
|
|
171
|
+
- **Order Items**: Product name, SKU, quantity, unit price, total amount
|
|
172
|
+
- **Billing Address**: City, country, name, address line, phone, email, postal code
|
|
173
|
+
- **Shipping Address**: City, country, name, address line, phone, email, postal code
|
|
174
|
+
- **Customer Info**: Name, phone, email, identity number
|
|
175
|
+
- **Amounts**: Total amount, shipping amount, currency
|
|
176
|
+
|
|
177
|
+
### Security
|
|
178
|
+
|
|
179
|
+
All data is sent with a SHA-512 hash for validation:
|
|
180
|
+
|
|
181
|
+
- Hash is generated using: `salt | sessionId | hashKey`
|
|
182
|
+
- Extension validates the hash before processing
|
|
183
|
+
|
|
184
|
+
## API Routes
|
|
185
|
+
|
|
186
|
+
### Check Availability API
|
|
187
|
+
|
|
188
|
+
To enable HASO payment availability checks, you need to create an API route. Create a file at `src/app/api/haso-check-availability/route.ts` with the following content:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { POST } from '@akinon/pz-haso/src/pages/api/check-availability';
|
|
192
|
+
|
|
193
|
+
export { POST };
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
This API endpoint handles checking the availability of HASO payment for a given:
|
|
197
|
+
|
|
198
|
+
- Phone number
|
|
199
|
+
- Order amount
|
|
200
|
+
- Email (optional)
|
|
201
|
+
- Identity number (optional)
|
|
202
|
+
|
|
203
|
+
The endpoint automatically validates the request and response using hash-based security measures.
|
|
204
|
+
|
|
205
|
+
### Using checkHasoAvailability Mutation
|
|
206
|
+
|
|
207
|
+
The extension provides a Redux mutation hook that you can use to check HASO payment availability. Here's an example of how to implement it:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { useCheckHasoAvailabilityMutation } from '@akinon/pz-haso';
|
|
211
|
+
|
|
212
|
+
const YourComponent = () => {
|
|
213
|
+
const [checkHasoAvailability] = useCheckHasoAvailabilityMutation();
|
|
214
|
+
const [isHasoAvailable, setIsHasoAvailable] = useState(false);
|
|
215
|
+
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
const checkAvailability = async () => {
|
|
218
|
+
try {
|
|
219
|
+
const response = await checkHasoAvailability({
|
|
220
|
+
amount: '1000.00',
|
|
221
|
+
phone: '+905551234567',
|
|
222
|
+
email: 'customer@example.com',
|
|
223
|
+
identity_number: '12345678901' // TC Kimlik No (optional)
|
|
224
|
+
}).unwrap();
|
|
225
|
+
|
|
226
|
+
setIsHasoAvailable(response.is_available);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error('Error checking HASO availability:', error);
|
|
229
|
+
setIsHasoAvailable(false);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
checkAvailability();
|
|
234
|
+
}, [checkHasoAvailability]);
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
// Your component JSX
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The mutation returns an object with the following properties:
|
|
243
|
+
|
|
244
|
+
- `is_available`: boolean indicating if HASO payment is available
|
|
245
|
+
- `limit`: number indicating the available credit limit (optional)
|
|
246
|
+
- `salt`: string used for hash verification
|
|
247
|
+
- `hash`: string for response validation
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@akinon/pz-haso",
|
|
3
|
+
"version": "1.112.0-snapshot-ZERO-3859-20251125183557",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"test:watch": "jest --watch"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"react": "^18.0.0",
|
|
12
|
+
"react-dom": "^18.0.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/jest": "^29.5.14",
|
|
16
|
+
"@types/node": "^18.7.8",
|
|
17
|
+
"@types/react": "^18.0.17",
|
|
18
|
+
"@types/react-dom": "^18.0.6",
|
|
19
|
+
"jest": "^29.7.0",
|
|
20
|
+
"prettier": "^3.0.3",
|
|
21
|
+
"react": "^18.2.0",
|
|
22
|
+
"react-dom": "^18.2.0",
|
|
23
|
+
"ts-jest": "^29.3.1",
|
|
24
|
+
"typescript": "^5.2.2"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
import { FormComponentProps } from '../types';
|
|
4
|
+
|
|
5
|
+
const FormComponent = ({
|
|
6
|
+
extensionUrl,
|
|
7
|
+
sessionId,
|
|
8
|
+
context,
|
|
9
|
+
renderer,
|
|
10
|
+
autoSubmit = true
|
|
11
|
+
}: FormComponentProps) => {
|
|
12
|
+
const nextCookies = cookies();
|
|
13
|
+
|
|
14
|
+
const extensionUrlWithoutSlash = `${extensionUrl}`.endsWith('/')
|
|
15
|
+
? extensionUrl.slice(0, -1)
|
|
16
|
+
: extensionUrl;
|
|
17
|
+
|
|
18
|
+
const csrfToken = nextCookies.get('csrftoken')?.value ?? '';
|
|
19
|
+
|
|
20
|
+
const DefaultForm = ({
|
|
21
|
+
extensionUrl,
|
|
22
|
+
sessionId,
|
|
23
|
+
context,
|
|
24
|
+
csrfToken,
|
|
25
|
+
autoSubmit
|
|
26
|
+
}: {
|
|
27
|
+
extensionUrl: string;
|
|
28
|
+
sessionId: string;
|
|
29
|
+
context: any;
|
|
30
|
+
csrfToken: string;
|
|
31
|
+
autoSubmit: boolean;
|
|
32
|
+
}) => (
|
|
33
|
+
<form
|
|
34
|
+
action={`${extensionUrl}/form-page/?sessionId=${sessionId}`}
|
|
35
|
+
method="post"
|
|
36
|
+
encType="multipart/form-data"
|
|
37
|
+
id="haso-extension-form"
|
|
38
|
+
>
|
|
39
|
+
<input type="hidden" name="csrf_token" value={csrfToken} />
|
|
40
|
+
<input type="hidden" name="data" value={JSON.stringify(context)} />
|
|
41
|
+
|
|
42
|
+
{autoSubmit && (
|
|
43
|
+
<script
|
|
44
|
+
dangerouslySetInnerHTML={{
|
|
45
|
+
__html: "document.getElementById('haso-extension-form').submit()"
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
)}
|
|
49
|
+
</form>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const RenderForm = renderer?.renderForm || DefaultForm;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<RenderForm
|
|
56
|
+
extensionUrl={extensionUrlWithoutSlash}
|
|
57
|
+
sessionId={sessionId}
|
|
58
|
+
context={context}
|
|
59
|
+
csrfToken={csrfToken}
|
|
60
|
+
autoSubmit={autoSubmit}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default FormComponent;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import settings from 'settings';
|
|
3
|
+
import { cookies } from 'next/headers';
|
|
4
|
+
import FormComponent from '../components/FormComponent';
|
|
5
|
+
import { fetchData, generateHash, getRandomString } from '../utils';
|
|
6
|
+
import { HasoPaymentGatewayProps } from '../types';
|
|
7
|
+
|
|
8
|
+
export const HasoPaymentGateway = async ({
|
|
9
|
+
sessionId,
|
|
10
|
+
currency,
|
|
11
|
+
locale,
|
|
12
|
+
extensionUrl,
|
|
13
|
+
hashKey,
|
|
14
|
+
renderer
|
|
15
|
+
}: HasoPaymentGatewayProps) => {
|
|
16
|
+
if (!sessionId || !currency || !locale) {
|
|
17
|
+
return <></>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const nextCookies = cookies();
|
|
21
|
+
|
|
22
|
+
const language = settings.localization.locales.find(
|
|
23
|
+
(item: { value: string; apiValue: string }) => item.value === locale
|
|
24
|
+
)?.apiValue;
|
|
25
|
+
|
|
26
|
+
const requestHeaders = {
|
|
27
|
+
Cookie: `osessionid=${nextCookies.get('osessionid')?.value}`,
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'X-Currency': currency,
|
|
30
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
31
|
+
'Accept-Language': language || 'tr-TR'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const preOrder = await fetchData(
|
|
35
|
+
`${settings.commerceUrl}/orders/checkout/?page=OrderNotePage`,
|
|
36
|
+
requestHeaders
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const salt = getRandomString(10);
|
|
40
|
+
const hash = generateHash(salt, sessionId, hashKey);
|
|
41
|
+
|
|
42
|
+
const context = {
|
|
43
|
+
salt,
|
|
44
|
+
hash,
|
|
45
|
+
shipping_address: {
|
|
46
|
+
first_name: preOrder.pre_order.shipping_address?.first_name,
|
|
47
|
+
last_name: preOrder.pre_order.shipping_address?.last_name,
|
|
48
|
+
phone_number: preOrder.pre_order.shipping_address?.phone_number,
|
|
49
|
+
line: preOrder.pre_order.shipping_address?.line,
|
|
50
|
+
district: preOrder.pre_order.shipping_address?.district?.name || null,
|
|
51
|
+
township: preOrder.pre_order.shipping_address?.township?.name,
|
|
52
|
+
city: preOrder.pre_order.shipping_address?.city?.name,
|
|
53
|
+
country: preOrder.pre_order.shipping_address?.country?.name,
|
|
54
|
+
zip_code: preOrder.pre_order.shipping_address?.postcode
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const DefaultContainer = ({
|
|
59
|
+
children
|
|
60
|
+
}: {
|
|
61
|
+
children: React.ReactNode;
|
|
62
|
+
}): React.ReactElement => <>{children}</>;
|
|
63
|
+
|
|
64
|
+
const RenderContainer =
|
|
65
|
+
renderer?.paymentGateway?.renderContainer || DefaultContainer;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<RenderContainer>
|
|
69
|
+
<FormComponent
|
|
70
|
+
extensionUrl={extensionUrl}
|
|
71
|
+
sessionId={sessionId}
|
|
72
|
+
context={context}
|
|
73
|
+
renderer={renderer?.formComponent}
|
|
74
|
+
/>
|
|
75
|
+
</RenderContainer>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import type { NextRequest } from 'next/server';
|
|
3
|
+
import { getRandomString, generateHash } from '../../utils';
|
|
4
|
+
|
|
5
|
+
interface CheckAvailabilityResponse {
|
|
6
|
+
salt: string;
|
|
7
|
+
hash: string;
|
|
8
|
+
is_available: boolean;
|
|
9
|
+
limit?: number;
|
|
10
|
+
message?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const verifyResponseHash = (
|
|
14
|
+
salt: string,
|
|
15
|
+
isAvailable: boolean,
|
|
16
|
+
hashKey: string,
|
|
17
|
+
responseHash: string
|
|
18
|
+
): boolean => {
|
|
19
|
+
return (
|
|
20
|
+
generateHash(salt, isAvailable ? 'True' : 'False', hashKey) === responseHash
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function POST(request: NextRequest) {
|
|
25
|
+
try {
|
|
26
|
+
const hashKey = process.env.HASO_HASH_KEY;
|
|
27
|
+
const extensionUrl = process.env.HASO_EXTENSION_URL;
|
|
28
|
+
|
|
29
|
+
if (!hashKey) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ message: 'HASO_HASH_KEY environment variable is not set' },
|
|
32
|
+
{ status: 500 }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!extensionUrl) {
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ message: 'HASO_EXTENSION_URL environment variable is not set' },
|
|
39
|
+
{ status: 500 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const body = await request.json();
|
|
44
|
+
const { amount, phone, email, identity_number } = body;
|
|
45
|
+
|
|
46
|
+
if (!amount || !phone) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ message: 'Missing required fields (amount, phone)' },
|
|
49
|
+
{ status: 400 }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const salt = getRandomString(10);
|
|
54
|
+
const hash = generateHash(salt, phone, amount, hashKey);
|
|
55
|
+
|
|
56
|
+
const extensionUrlWithoutSlash = extensionUrl.endsWith('/')
|
|
57
|
+
? extensionUrl.slice(0, -1)
|
|
58
|
+
: extensionUrl;
|
|
59
|
+
|
|
60
|
+
const response = await fetch(
|
|
61
|
+
`${extensionUrlWithoutSlash}/check-availability`,
|
|
62
|
+
{
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json'
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
salt,
|
|
69
|
+
hash,
|
|
70
|
+
amount,
|
|
71
|
+
phone,
|
|
72
|
+
email,
|
|
73
|
+
identity_number
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const errorData = await response.json();
|
|
80
|
+
return NextResponse.json(errorData, { status: response.status });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const data: CheckAvailabilityResponse = await response.json();
|
|
84
|
+
|
|
85
|
+
const isValidHash = verifyResponseHash(
|
|
86
|
+
data.salt,
|
|
87
|
+
data.is_available,
|
|
88
|
+
hashKey,
|
|
89
|
+
data.hash
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (!isValidHash) {
|
|
93
|
+
return NextResponse.json(
|
|
94
|
+
{ message: 'Invalid response hash' },
|
|
95
|
+
{ status: 400 }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return NextResponse.json(data);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Error checking HASO availability:', error);
|
|
102
|
+
return NextResponse.json(
|
|
103
|
+
{ message: 'Internal server error' },
|
|
104
|
+
{ status: 500 }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/redux/api.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { api } from '@akinon/next/data/client/api';
|
|
2
|
+
import { setPaymentStepBusy } from '@akinon/next/redux/reducers/checkout';
|
|
3
|
+
|
|
4
|
+
interface CheckHasoAvailabilityRequest {
|
|
5
|
+
amount: string;
|
|
6
|
+
phone: string;
|
|
7
|
+
email: string;
|
|
8
|
+
identity_number?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface CheckHasoAvailabilityResponse {
|
|
12
|
+
salt: string;
|
|
13
|
+
hash: string;
|
|
14
|
+
is_available: boolean;
|
|
15
|
+
limit?: number;
|
|
16
|
+
message?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const hasoApi = api.injectEndpoints({
|
|
20
|
+
endpoints: (build) => ({
|
|
21
|
+
checkHasoAvailability: build.mutation<
|
|
22
|
+
CheckHasoAvailabilityResponse,
|
|
23
|
+
CheckHasoAvailabilityRequest
|
|
24
|
+
>({
|
|
25
|
+
query: (body) => ({
|
|
26
|
+
url: '/api/haso-check-availability/',
|
|
27
|
+
method: 'POST',
|
|
28
|
+
body
|
|
29
|
+
}),
|
|
30
|
+
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
|
31
|
+
dispatch(setPaymentStepBusy(true));
|
|
32
|
+
await queryFulfilled;
|
|
33
|
+
dispatch(setPaymentStepBusy(false));
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const { useCheckHasoAvailabilityMutation } = hasoApi;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface HasoRendererProps {
|
|
4
|
+
formComponent?: {
|
|
5
|
+
renderForm?: (props: {
|
|
6
|
+
extensionUrl: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
context: any;
|
|
9
|
+
csrfToken: string;
|
|
10
|
+
autoSubmit: boolean;
|
|
11
|
+
}) => React.ReactNode;
|
|
12
|
+
renderLoading?: () => React.ReactNode;
|
|
13
|
+
renderError?: (error: string) => React.ReactNode;
|
|
14
|
+
};
|
|
15
|
+
paymentGateway?: {
|
|
16
|
+
renderContainer?: (props: { children: React.ReactNode }) => React.ReactNode;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FormComponentProps {
|
|
21
|
+
extensionUrl: string;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
context: any;
|
|
24
|
+
renderer?: HasoRendererProps['formComponent'];
|
|
25
|
+
autoSubmit?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface HasoPaymentGatewayProps {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
currency: string;
|
|
31
|
+
locale: string;
|
|
32
|
+
extensionUrl: string;
|
|
33
|
+
hashKey: string;
|
|
34
|
+
renderer?: HasoRendererProps;
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export const fetchData = async (url: string, requestHeaders: HeadersInit) => {
|
|
4
|
+
const response = await fetch(url, { headers: requestHeaders });
|
|
5
|
+
return response.json();
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const getRandomString = (length: number): string => {
|
|
9
|
+
const characters =
|
|
10
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
11
|
+
return Array.from({ length }, () =>
|
|
12
|
+
characters.charAt(Math.floor(Math.random() * characters.length))
|
|
13
|
+
).join('');
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const generateHash = (...values: string[]): string => {
|
|
17
|
+
const hashStr = values.join('|');
|
|
18
|
+
return crypto.createHash('sha512').update(hashStr, 'utf8').digest('hex');
|
|
19
|
+
};
|