@akinon/pz-tabby-extension 1.55.0
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 +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
|
+
};
|