@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 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
+ };