@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 ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "tabWidth": 2
6
+ }
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,7 @@
1
+ export * from './pages/HasoPaymentGateway';
2
+ export { useCheckHasoAvailabilityMutation } from './redux/api';
3
+ export type {
4
+ HasoPaymentGatewayProps,
5
+ HasoRendererProps,
6
+ FormComponentProps
7
+ } from './types';
@@ -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
+ }
@@ -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
+ };