@akinon/pz-tabby-extension 1.55.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.prettierrc +13 -0
- package/package.json +19 -0
- package/readme.md +59 -0
- package/src/components/FormComponent.tsx +48 -0
- package/src/index.tsx +1 -0
- package/src/pages/TabbyPaymentGateway.tsx +104 -0
- package/src/types/index.ts +28 -0
- package/src/utils/index.ts +66 -0
package/.prettierrc
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
{
|
2
|
+
"bracketSameLine": false,
|
3
|
+
"tabWidth": 2,
|
4
|
+
"singleQuote": true,
|
5
|
+
"jsxSingleQuote": false,
|
6
|
+
"bracketSpacing": true,
|
7
|
+
"semi": true,
|
8
|
+
"useTabs": false,
|
9
|
+
"arrowParens": "always",
|
10
|
+
"endOfLine": "lf",
|
11
|
+
"proseWrap": "never",
|
12
|
+
"trailingComma": "none"
|
13
|
+
}
|
package/package.json
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
{
|
2
|
+
"name": "@akinon/pz-tabby-extension",
|
3
|
+
"version": "1.55.0",
|
4
|
+
"license": "MIT",
|
5
|
+
"main": "src/index.tsx",
|
6
|
+
"peerDependencies": {
|
7
|
+
"react": "^18.0.0",
|
8
|
+
"react-dom": "^18.0.0"
|
9
|
+
},
|
10
|
+
"devDependencies": {
|
11
|
+
"@types/node": "^18.7.8",
|
12
|
+
"@types/react": "^18.0.17",
|
13
|
+
"@types/react-dom": "^18.0.6",
|
14
|
+
"prettier": "^3.0.3",
|
15
|
+
"react": "^18.2.0",
|
16
|
+
"react-dom": "^18.2.0",
|
17
|
+
"typescript": "^5.2.2"
|
18
|
+
}
|
19
|
+
}
|
package/readme.md
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# Tabby Payment Gateway Extension
|
2
|
+
|
3
|
+
## Installation
|
4
|
+
|
5
|
+
There are two ways to install the Tabby Payment Gateway extension:
|
6
|
+
|
7
|
+
### 1. Install the npm package using Yarn
|
8
|
+
|
9
|
+
For the latest version, you can install the package using Yarn:
|
10
|
+
|
11
|
+
```bash
|
12
|
+
yarn add @akinon/pz-tabby-extension
|
13
|
+
```
|
14
|
+
|
15
|
+
### 2. Preferred installation method
|
16
|
+
|
17
|
+
You can also use the following command to install the extension with the latest plugins:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
npx @akinon/projectzero@latest --plugins
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Once the extension is installed, you can easily integrate the Tabby payment gateway into your application. Here's an example of how to use it.
|
26
|
+
|
27
|
+
1. Navigate to the `src/app/[commerce]/[locale]/[currency]/payment-gateway/tabby/` directory.
|
28
|
+
|
29
|
+
2. Create a file named `page.tsx` and include the following code:
|
30
|
+
|
31
|
+
```jsx
|
32
|
+
import { TabbyPaymentGateway } from '@akinon/pz-tabby-extension';
|
33
|
+
|
34
|
+
const TabbyGateway = ({
|
35
|
+
searchParams: { sessionId },
|
36
|
+
params: { currency }
|
37
|
+
}: {
|
38
|
+
searchParams: Record<string, string>;
|
39
|
+
params: { currency: string };
|
40
|
+
}) => (
|
41
|
+
<TabbyPaymentGateway
|
42
|
+
currency={currency}
|
43
|
+
extensionUrl={process.env.TABBY_EXTENSION_URL}
|
44
|
+
hashKey={process.env.TABBY_HASH_KEY}
|
45
|
+
sessionId={sessionId}
|
46
|
+
/>
|
47
|
+
);
|
48
|
+
|
49
|
+
export default TabbyGateway;
|
50
|
+
```
|
51
|
+
|
52
|
+
## Configuration
|
53
|
+
|
54
|
+
Add these variables to your `.env` file
|
55
|
+
|
56
|
+
```env
|
57
|
+
TABBY_EXTENSION_URL=<your_extension_url>
|
58
|
+
TABBY_HASH_KEY=<your_hash_key>
|
59
|
+
```
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { cookies } from 'next/headers';
|
3
|
+
|
4
|
+
type FormComponentProps = {
|
5
|
+
extensionUrl: string;
|
6
|
+
sessionId: string;
|
7
|
+
hash: string;
|
8
|
+
salt: string;
|
9
|
+
context: any;
|
10
|
+
};
|
11
|
+
|
12
|
+
const FormComponent = ({
|
13
|
+
extensionUrl,
|
14
|
+
sessionId,
|
15
|
+
hash,
|
16
|
+
salt,
|
17
|
+
context
|
18
|
+
}: FormComponentProps) => {
|
19
|
+
const nextCookies = cookies();
|
20
|
+
|
21
|
+
const extensionUrlWithoutSlash = `${extensionUrl}`.endsWith('/')
|
22
|
+
? extensionUrl.slice(0, -1)
|
23
|
+
: extensionUrl;
|
24
|
+
|
25
|
+
const csrfToken = nextCookies.get('csrftoken')?.value ?? '';
|
26
|
+
|
27
|
+
return (
|
28
|
+
<form
|
29
|
+
action={`${extensionUrlWithoutSlash}/form-page/?sessionId=${sessionId}`}
|
30
|
+
method="post"
|
31
|
+
encType="multipart/form-data"
|
32
|
+
id="tabby-extension-form"
|
33
|
+
>
|
34
|
+
<input type="hidden" name="csrf_token" value={csrfToken} />
|
35
|
+
<input type="hidden" name="hash" value={hash} />
|
36
|
+
<input type="hidden" name="salt" value={salt} />
|
37
|
+
<input type="hidden" name="data" value={JSON.stringify(context)} />
|
38
|
+
|
39
|
+
<script
|
40
|
+
dangerouslySetInnerHTML={{
|
41
|
+
__html: "document.getElementById('tabby-extension-form').submit()"
|
42
|
+
}}
|
43
|
+
/>
|
44
|
+
</form>
|
45
|
+
);
|
46
|
+
};
|
47
|
+
|
48
|
+
export default FormComponent;
|
package/src/index.tsx
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export * from './pages/TabbyPaymentGateway';
|
@@ -0,0 +1,104 @@
|
|
1
|
+
import { URLS } from '@akinon/next/data/urls';
|
2
|
+
import settings from '@theme/settings';
|
3
|
+
import { cookies } from 'next/headers';
|
4
|
+
import React from 'react';
|
5
|
+
import FormComponent from '../components/FormComponent';
|
6
|
+
import {
|
7
|
+
fetchData,
|
8
|
+
generateHash,
|
9
|
+
getOrderStatus,
|
10
|
+
getRandomString,
|
11
|
+
groupByProductId
|
12
|
+
} from '../utils';
|
13
|
+
|
14
|
+
type TabbyPaymentGatewayProps = {
|
15
|
+
sessionId: string;
|
16
|
+
currency: string;
|
17
|
+
extensionUrl: string;
|
18
|
+
hashKey: string;
|
19
|
+
};
|
20
|
+
|
21
|
+
export const TabbyPaymentGateway = async ({
|
22
|
+
sessionId,
|
23
|
+
currency,
|
24
|
+
extensionUrl,
|
25
|
+
hashKey
|
26
|
+
}: TabbyPaymentGatewayProps) => {
|
27
|
+
const nextCookies = cookies();
|
28
|
+
|
29
|
+
const requestHeaders = {
|
30
|
+
Cookie: `osessionid=${nextCookies.get('osessionid')?.value}`,
|
31
|
+
'Content-Type': 'application/json',
|
32
|
+
'x-currency': currency,
|
33
|
+
'X-Requested-With': 'XMLHttpRequest'
|
34
|
+
};
|
35
|
+
|
36
|
+
const [preOrder, userProfile, successOrders, orderHistory, wishlist] =
|
37
|
+
await Promise.all([
|
38
|
+
fetchData(
|
39
|
+
`${settings.commerceUrl}/orders/checkout/?page=OrderNotePage`,
|
40
|
+
requestHeaders
|
41
|
+
),
|
42
|
+
fetchData(URLS.user.currentUser, requestHeaders),
|
43
|
+
fetchData(`${URLS.account.order}?limit=1&status=550`, requestHeaders),
|
44
|
+
fetchData(`${URLS.account.order}?limit=10`, requestHeaders),
|
45
|
+
fetchData(
|
46
|
+
URLS.wishlist.getFavorites({ page: 1, limit: 1 }),
|
47
|
+
requestHeaders
|
48
|
+
)
|
49
|
+
]);
|
50
|
+
|
51
|
+
const salt = getRandomString(10);
|
52
|
+
const hash = generateHash(hashKey, sessionId, salt);
|
53
|
+
|
54
|
+
const context = {
|
55
|
+
salt,
|
56
|
+
hash,
|
57
|
+
shipping_address: {
|
58
|
+
city: preOrder.pre_order.shipping_address.city.name,
|
59
|
+
address: preOrder.pre_order.shipping_address.line,
|
60
|
+
zip: preOrder.pre_order.shipping_address.postcode
|
61
|
+
},
|
62
|
+
order_items: preOrder.pre_order.basket.basketitem_set.map((item: any) => ({
|
63
|
+
unit_price: item.unit_price,
|
64
|
+
title: item.product.name,
|
65
|
+
quantity: item.quantity,
|
66
|
+
category: item.product.category.name
|
67
|
+
})),
|
68
|
+
buyer_history: {
|
69
|
+
registered_since: userProfile.date_joined,
|
70
|
+
loyalty_level: successOrders.count,
|
71
|
+
wishlist_count: wishlist.count,
|
72
|
+
is_email_verified: userProfile.is_email_verified,
|
73
|
+
is_social_networks_connected: userProfile.is_social_networks_connected,
|
74
|
+
is_phone_number_verified: userProfile.attributes?.verified_phone ?? false
|
75
|
+
},
|
76
|
+
order_history: orderHistory.results.map((order_history: any) => ({
|
77
|
+
purchased_at: order_history.created_date,
|
78
|
+
amount: order_history.amount,
|
79
|
+
payment_method: order_history.card === null ? 'cod' : 'card',
|
80
|
+
status: getOrderStatus(order_history.status.value),
|
81
|
+
buyer: {
|
82
|
+
phone: order_history.billing_address.phone_number,
|
83
|
+
email: order_history.billing_address.email,
|
84
|
+
name: `${order_history.billing_address.first_name} ${order_history.billing_address.last_name}`
|
85
|
+
},
|
86
|
+
shipping_address: {
|
87
|
+
city: order_history.shipping_address.city.name,
|
88
|
+
address: order_history.shipping_address.line,
|
89
|
+
zip: order_history.shipping_address.postcode
|
90
|
+
},
|
91
|
+
order_items: groupByProductId(order_history.orderitem_set)
|
92
|
+
}))
|
93
|
+
};
|
94
|
+
|
95
|
+
return (
|
96
|
+
<FormComponent
|
97
|
+
extensionUrl={extensionUrl}
|
98
|
+
sessionId={sessionId}
|
99
|
+
hash={hash}
|
100
|
+
salt={salt}
|
101
|
+
context={context}
|
102
|
+
/>
|
103
|
+
);
|
104
|
+
};
|
@@ -0,0 +1,28 @@
|
|
1
|
+
export enum OrderServiceStatus {
|
2
|
+
New = 'new',
|
3
|
+
Processing = 'processing',
|
4
|
+
Completed = 'complete',
|
5
|
+
Refunded = 'refunded',
|
6
|
+
Canceled = 'canceled',
|
7
|
+
Unknown = 'unknown'
|
8
|
+
}
|
9
|
+
|
10
|
+
export enum OrderCommerceStatus {
|
11
|
+
CancellationWaiting = '50',
|
12
|
+
Cancelled = '100',
|
13
|
+
Waiting = '200',
|
14
|
+
PaymentWaiting = '300',
|
15
|
+
ConfirmationWaiting = '350',
|
16
|
+
Approved = '400',
|
17
|
+
Preparing = '450',
|
18
|
+
Shipped = '500',
|
19
|
+
ShippedAndInformed = '510',
|
20
|
+
ReadyForPickup = '520',
|
21
|
+
AttemptedDelivery = '540',
|
22
|
+
ReviewStarted = '544',
|
23
|
+
ReviewWaiting = '545',
|
24
|
+
WaitingForPayment = '546',
|
25
|
+
Paid = '547',
|
26
|
+
Delivered = '550',
|
27
|
+
Refunded = '600'
|
28
|
+
}
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import crypto from 'crypto';
|
2
|
+
import { OrderCommerceStatus, OrderServiceStatus } from '../types';
|
3
|
+
|
4
|
+
export const fetchData = async (url: string, requestHeaders: HeadersInit) => {
|
5
|
+
const response = await fetch(url, { headers: requestHeaders });
|
6
|
+
return response.json();
|
7
|
+
};
|
8
|
+
|
9
|
+
export const getRandomString = (length: number): string => {
|
10
|
+
const characters =
|
11
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
12
|
+
return Array.from({ length }, () =>
|
13
|
+
characters.charAt(Math.floor(Math.random() * characters.length))
|
14
|
+
).join('');
|
15
|
+
};
|
16
|
+
|
17
|
+
export const generateHash = (
|
18
|
+
hashKey: string,
|
19
|
+
sessionId: string,
|
20
|
+
salt: string
|
21
|
+
): string => {
|
22
|
+
const hashStr = `${salt}|${sessionId}|${hashKey}`;
|
23
|
+
return crypto.createHash('sha512').update(hashStr, 'utf8').digest('hex');
|
24
|
+
};
|
25
|
+
|
26
|
+
export const groupByProductId = (orderItems: any[]) => {
|
27
|
+
return Object.values(
|
28
|
+
orderItems.reduce((acc, item) => {
|
29
|
+
const productId = item.product.pk;
|
30
|
+
acc[productId] = acc[productId] || {
|
31
|
+
unit_price: item.price,
|
32
|
+
title: item.product.name,
|
33
|
+
quantity: 0,
|
34
|
+
category: item.product.category.name
|
35
|
+
};
|
36
|
+
acc[productId].quantity += 1;
|
37
|
+
return acc;
|
38
|
+
}, {})
|
39
|
+
);
|
40
|
+
};
|
41
|
+
|
42
|
+
export const getOrderStatus = (
|
43
|
+
commerceOrderStatusValue: string
|
44
|
+
): OrderServiceStatus => {
|
45
|
+
switch (commerceOrderStatusValue) {
|
46
|
+
case OrderCommerceStatus.Waiting:
|
47
|
+
case OrderCommerceStatus.PaymentWaiting:
|
48
|
+
case OrderCommerceStatus.ConfirmationWaiting:
|
49
|
+
return OrderServiceStatus.New;
|
50
|
+
case OrderCommerceStatus.Approved:
|
51
|
+
case OrderCommerceStatus.Preparing:
|
52
|
+
case OrderCommerceStatus.Shipped:
|
53
|
+
case OrderCommerceStatus.ShippedAndInformed:
|
54
|
+
case OrderCommerceStatus.ReadyForPickup:
|
55
|
+
case OrderCommerceStatus.AttemptedDelivery:
|
56
|
+
return OrderServiceStatus.Processing;
|
57
|
+
case OrderCommerceStatus.Delivered:
|
58
|
+
return OrderServiceStatus.Completed;
|
59
|
+
case OrderCommerceStatus.Refunded:
|
60
|
+
return OrderServiceStatus.Refunded;
|
61
|
+
case OrderCommerceStatus.Cancelled:
|
62
|
+
return OrderServiceStatus.Canceled;
|
63
|
+
default:
|
64
|
+
return OrderServiceStatus.Unknown;
|
65
|
+
}
|
66
|
+
};
|