@akinon/pz-tabby-extension 2.0.0-beta.2 → 2.0.0-beta.21

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/CHANGELOG.md CHANGED
@@ -1,18 +1,131 @@
1
1
  # @akinon/pz-tabby-extension
2
2
 
3
- ## 2.0.0-beta.2
3
+ ## 2.0.0-beta.21
4
4
 
5
- ## 2.0.0-beta.1
5
+ ## 2.0.0-beta.20
6
+
7
+ ## 1.126.0
8
+
9
+ ## 1.125.2
10
+
11
+ ## 1.125.1
12
+
13
+ ## 1.125.0
14
+
15
+ ## 1.124.0
16
+
17
+ ## 1.123.0
18
+
19
+ ## 1.122.0
20
+
21
+ ## 1.121.0
22
+
23
+ ## 1.120.0
24
+
25
+ ## 1.119.0
26
+
27
+ ## 1.118.0
28
+
29
+ ## 1.117.0
30
+
31
+ ## 1.116.0
32
+
33
+ ## 1.115.0
34
+
35
+ ## 1.114.0
36
+
37
+ ## 1.113.0
38
+
39
+ ## 1.112.0
40
+
41
+ ## 1.111.0
42
+
43
+ ## 1.110.0
44
+
45
+ ## 1.109.0
46
+
47
+ ## 1.108.0
48
+
49
+ ## 1.107.0
50
+
51
+ ## 1.106.0
52
+
53
+ ## 1.105.0
54
+
55
+ ## 1.104.0
56
+
57
+ ## 1.103.0
58
+
59
+ ## 1.102.0
60
+
61
+ ## 1.101.0
62
+
63
+ ## 1.100.0
64
+
65
+ ## 1.99.0
6
66
 
7
67
  ### Minor Changes
8
68
 
9
- - ZERO-3091: Upgrade Next.js to v15 and React to v19
69
+ - d58538b: ZERO-3638: Enhance RC pipeline: add fetch, merge, and pre-release setup with conditional commit
70
+
71
+ ## 1.98.0
72
+
73
+ ## 1.97.0
74
+
75
+ ## 1.96.0
76
+
77
+ ## 1.95.0
78
+
79
+ ### Minor Changes
80
+
81
+ - 1913efc: ZERO-3296: Add Tabby payment availability check API and update related components
82
+ - 2e3e8ab: ZERO-3296: Refactor POST handler to check currency-specific environment variables for hash key and extension URL
83
+
84
+ ## 1.94.0
85
+
86
+ ## 1.93.0
87
+
88
+ ## 1.92.0
89
+
90
+ ## 1.91.0
91
+
92
+ ## 1.90.0
93
+
94
+ ## 1.89.0
95
+
96
+ ## 1.88.0
97
+
98
+ ## 1.87.0
99
+
100
+ ## 1.86.0
101
+
102
+ ## 1.85.0
103
+
104
+ ## 1.84.0
105
+
106
+ ### Minor Changes
107
+
108
+ - 624a4eb: ZERO-3276: Update installation instructions across multiple README files to standardize format and improve clarity
109
+
110
+ ## 1.83.0
111
+
112
+ ## 1.82.0
113
+
114
+ ### Minor Changes
115
+
116
+ - 9db58ad: ZERO-3230: add default values to tabby payment gateway
117
+
118
+ ## 1.81.0
119
+
120
+ ## 1.80.0
121
+
122
+ ## 1.79.0
10
123
 
11
- ## 2.0.0-beta.0
124
+ ## 1.78.0
12
125
 
13
- ### Major Changes
126
+ ## 1.77.0
14
127
 
15
- - be6c09d: ZERO-3114: Create beta version.
128
+ ## 1.76.0
16
129
 
17
130
  ## 1.75.0
18
131
 
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@akinon/pz-tabby-extension",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.21",
4
4
  "license": "MIT",
5
5
  "main": "src/index.tsx",
6
6
  "peerDependencies": {
7
- "react": "^19.0.0",
8
- "react-dom": "^19.0.0"
7
+ "react": "^18.0.0 || ^19.0.0",
8
+ "react-dom": "^18.0.0 || ^19.0.0"
9
9
  },
10
10
  "devDependencies": {
11
- "@types/node": "^22.10.2",
12
- "@types/react": "^19.0.2",
13
- "@types/react-dom": "^19.0.2",
14
- "prettier": "^3.4.2",
15
- "react": "^19.0.0",
16
- "react-dom": "^19.0.0",
17
- "typescript": "^5.7.2"
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": "19.2.5",
16
+ "react-dom": "19.2.5",
17
+ "typescript": "^5.2.2"
18
18
  }
19
19
  }
package/readme.md CHANGED
@@ -2,19 +2,7 @@
2
2
 
3
3
  ## Installation
4
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:
5
+ You can use the following command to install the extension with the latest plugins:
18
6
 
19
7
  ```bash
20
8
  npx @akinon/projectzero@latest --plugins
@@ -52,6 +40,71 @@ const TabbyGateway = async ({
52
40
  export default TabbyGateway;
53
41
  ```
54
42
 
43
+ ## API Routes
44
+
45
+ ### Check Availability API
46
+
47
+ To enable Tabby payment availability checks, you need to create an API route. Create a file at `src/app/api/tabby-check-availability/route.ts` with the following content:
48
+
49
+ ```typescript
50
+ import { POST } from '@akinon/pz-tabby-extension/src/pages/api/check-availability';
51
+
52
+ export { POST };
53
+ ```
54
+
55
+ This API endpoint handles checking the availability of Tabby payment for a given:
56
+
57
+ - Order amount
58
+ - Email
59
+ - Phone number
60
+ - Currency
61
+
62
+ The endpoint automatically validates the request and response using hash-based security measures.
63
+
64
+ ### Using checkTabbyAvailability Mutation
65
+
66
+ The extension provides a Redux mutation hook that you can use to check Tabby payment availability. Here's an example of how to implement it:
67
+
68
+ ```typescript
69
+ import { useCheckTabbyAvailabilityMutation } from '@akinon/pz-tabby-extension/src/redux/api';
70
+
71
+ const YourComponent = () => {
72
+ const [checkTabbyAvailability] = useCheckTabbyAvailabilityMutation();
73
+ const [isTabbyAvailable, setIsTabbyAvailable] = useState(false);
74
+
75
+ useEffect(() => {
76
+ const checkAvailability = async () => {
77
+ try {
78
+ const response = await checkTabbyAvailability({
79
+ amount: 1000, // Order total amount
80
+ phone: '+971123456789', // Customer's phone number
81
+ email: 'example@example.com', // Customer's email ,
82
+ name: 'Akinon Akinon', // Customer's Full Name
83
+ }).unwrap();
84
+
85
+ setIsTabbyAvailable(response.is_available);
86
+ } catch (error) {
87
+ console.error('Error checking Tabby availability:', error);
88
+ setIsTabbyAvailable(false);
89
+ }
90
+ };
91
+
92
+ checkAvailability();
93
+ }, [checkTabbyAvailability]);
94
+
95
+ // Use isTabbyAvailable to conditionally render Tabby payment option
96
+ return (
97
+ // Your component JSX
98
+ );
99
+ };
100
+ ```
101
+
102
+ The mutation returns an object with the following properties:
103
+
104
+ - `is_available`: boolean indicating if Tabby payment is available
105
+ - `salt`: string used for hash verification
106
+ - `hash`: string for response validation
107
+
55
108
  ## Configuration
56
109
 
57
110
  Add these variables to your `.env` file
@@ -59,4 +112,4 @@ Add these variables to your `.env` file
59
112
  ```env
60
113
  TABBY_EXTENSION_URL=<your_extension_url>
61
114
  TABBY_HASH_KEY=<your_hash_key>
62
- ```
115
+ ```
@@ -6,6 +6,7 @@ import FormComponent from '../components/FormComponent';
6
6
  import {
7
7
  fetchData,
8
8
  generateHash,
9
+ getISODateWithMicroseconds,
9
10
  getOrderStatus,
10
11
  getRandomString,
11
12
  groupByProductId
@@ -60,47 +61,52 @@ export const TabbyPaymentGateway = async ({
60
61
  ]);
61
62
 
62
63
  const salt = getRandomString(10);
63
- const hash = generateHash(hashKey, sessionId, salt);
64
+ const hash = generateHash(salt, sessionId, hashKey);
64
65
 
65
66
  const context = {
66
67
  salt,
67
68
  hash,
68
69
  shipping_address: {
69
- city: preOrder.pre_order.shipping_address.city.name,
70
- address: preOrder.pre_order.shipping_address.line,
71
- zip: preOrder.pre_order.shipping_address.postcode ?? '0'
70
+ city: preOrder?.pre_order?.shipping_address?.city?.name,
71
+ address: preOrder?.pre_order?.shipping_address?.line,
72
+ zip: preOrder?.pre_order?.shipping_address?.postcode ?? '0'
72
73
  },
73
- order_items: preOrder.pre_order.basket.basketitem_set.map((item: any) => ({
74
- unit_price: item.unit_price,
75
- title: item.product.name,
76
- quantity: item.quantity,
77
- category: item.product.category.name
78
- })),
74
+ order_items: preOrder?.pre_order?.basket?.basketitem_set?.map(
75
+ (item: any) => ({
76
+ unit_price: item?.unit_price,
77
+ title: item?.product?.name,
78
+ quantity: item?.quantity,
79
+ category: item?.product?.category?.name
80
+ })
81
+ ),
79
82
  buyer_history: {
80
- registered_since: userProfile.date_joined,
81
- loyalty_level: successOrders.count,
82
- wishlist_count: wishlist.count,
83
- is_email_verified: userProfile.is_email_verified,
84
- is_social_networks_connected: userProfile.is_social_networks_connected,
85
- is_phone_number_verified: userProfile.attributes?.verified_phone ?? false
83
+ registered_since:
84
+ userProfile?.date_joined ?? getISODateWithMicroseconds(),
85
+ loyalty_level: successOrders?.count ?? 0,
86
+ wishlist_count: wishlist?.count ?? 0,
87
+ is_email_verified: userProfile?.is_email_verified ?? false,
88
+ is_social_networks_connected:
89
+ userProfile?.is_social_networks_connected ?? false,
90
+ is_phone_number_verified: userProfile?.attributes?.verified_phone ?? false
86
91
  },
87
- order_history: orderHistory.results.map((order_history: any) => ({
88
- purchased_at: order_history.created_date,
89
- amount: order_history.amount,
90
- payment_method: order_history.card === null ? 'cod' : 'card',
91
- status: getOrderStatus(order_history.status.value),
92
- buyer: {
93
- phone: order_history.billing_address.phone_number,
94
- email: order_history.billing_address.email,
95
- name: `${order_history.billing_address.first_name} ${order_history.billing_address.last_name}`
96
- },
97
- shipping_address: {
98
- city: order_history.shipping_address.city.name,
99
- address: order_history.shipping_address.line,
100
- zip: order_history.shipping_address.postcode ?? '0'
101
- },
102
- order_items: groupByProductId(order_history.orderitem_set)
103
- }))
92
+ order_history:
93
+ orderHistory?.results?.map((order_history: any) => ({
94
+ purchased_at: order_history?.created_date,
95
+ amount: order_history?.amount,
96
+ payment_method: order_history?.card === null ? 'cod' : 'card',
97
+ status: getOrderStatus(order_history?.status?.value),
98
+ buyer: {
99
+ phone: order_history?.billing_address?.phone_number,
100
+ email: order_history?.billing_address?.email,
101
+ name: `${order_history?.billing_address?.first_name} ${order_history?.billing_address?.last_name}`
102
+ },
103
+ shipping_address: {
104
+ city: order_history?.shipping_address?.city?.name,
105
+ address: order_history?.shipping_address?.line,
106
+ zip: order_history?.shipping_address?.postcode ?? '0'
107
+ },
108
+ order_items: groupByProductId(order_history?.orderitem_set)
109
+ })) ?? []
104
110
  };
105
111
 
106
112
  return (
@@ -0,0 +1,118 @@
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
+ }
10
+ const verifyResponseHash = (
11
+ salt: string,
12
+ hasAvailability: boolean,
13
+ hashKey: string,
14
+ responseHash: string
15
+ ): boolean => {
16
+ return (
17
+ generateHash(salt, hasAvailability ? 'True' : 'False', hashKey) ===
18
+ responseHash
19
+ );
20
+ };
21
+
22
+ export async function POST(request: NextRequest) {
23
+ try {
24
+ const body = await request.json();
25
+ const { amount, phone, email, name } = body;
26
+ const currency = request.cookies.get('pz-currency')?.value;
27
+
28
+ if (!currency) {
29
+ return NextResponse.json(
30
+ { message: 'Currency not found in cookies' },
31
+ { status: 400 }
32
+ );
33
+ }
34
+
35
+ if (!amount || !phone || !email || !name) {
36
+ return NextResponse.json(
37
+ { message: 'Missing required fields' },
38
+ { status: 400 }
39
+ );
40
+ }
41
+
42
+ const upperCurrency = currency.toUpperCase();
43
+
44
+ const currencySpecificHashKey =
45
+ process.env[`TABBY_HASH_KEY_${upperCurrency}`];
46
+ const currencySpecificUrl =
47
+ process.env[`TABBY_EXTENSION_URL_${upperCurrency}`];
48
+
49
+ const hashKey = currencySpecificHashKey || process.env.TABBY_HASH_KEY;
50
+ const extensionUrl = currencySpecificUrl || process.env.TABBY_EXTENSION_URL;
51
+
52
+ if (!hashKey) {
53
+ return NextResponse.json(
54
+ { message: 'TABBY_HASH_KEY environment variable is not set' },
55
+ { status: 500 }
56
+ );
57
+ }
58
+
59
+ if (!extensionUrl) {
60
+ return NextResponse.json(
61
+ { message: 'TABBY_EXTENSION_URL environment variable is not set' },
62
+ { status: 500 }
63
+ );
64
+ }
65
+
66
+ const salt = getRandomString(10);
67
+ const hash = generateHash(salt, amount, email, phone, hashKey);
68
+
69
+ const response = await fetch(`${extensionUrl}/check-availability`, {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/json'
73
+ },
74
+ body: JSON.stringify({
75
+ salt,
76
+ hash,
77
+ payment: {
78
+ amount,
79
+ currency: upperCurrency,
80
+ buyer: {
81
+ phone,
82
+ email,
83
+ name
84
+ }
85
+ }
86
+ })
87
+ });
88
+
89
+ if (!response.ok) {
90
+ const errorData = await response.json();
91
+ return NextResponse.json(errorData, { status: response.status });
92
+ }
93
+
94
+ const data: CheckAvailabilityResponse = await response.json();
95
+
96
+ const isValidHash = verifyResponseHash(
97
+ data.salt,
98
+ data.is_available,
99
+ hashKey,
100
+ data.hash
101
+ );
102
+
103
+ if (!isValidHash) {
104
+ return NextResponse.json(
105
+ { message: 'Invalid response hash' },
106
+ { status: 400 }
107
+ );
108
+ }
109
+
110
+ return NextResponse.json(data);
111
+ } catch (error) {
112
+ console.error('Error checking Tabby availability:', error);
113
+ return NextResponse.json(
114
+ { message: 'Internal server error' },
115
+ { status: 500 }
116
+ );
117
+ }
118
+ }
@@ -0,0 +1,37 @@
1
+ import { api } from '@akinon/next/data/client/api';
2
+ import { setPaymentStepBusy } from '@akinon/next/redux/reducers/checkout';
3
+
4
+ interface CheckTabbyAvailabilityRequest {
5
+ amount: string;
6
+ phone: string;
7
+ email: string;
8
+ name: string;
9
+ }
10
+
11
+ interface CheckTabbyAvailabilityResponse {
12
+ salt: string;
13
+ hash: string;
14
+ is_available: boolean;
15
+ }
16
+
17
+ export const tabbyApi = api.injectEndpoints({
18
+ endpoints: (build) => ({
19
+ checkTabbyAvailability: build.mutation<
20
+ CheckTabbyAvailabilityResponse,
21
+ CheckTabbyAvailabilityRequest
22
+ >({
23
+ query: (body) => ({
24
+ url: '/api/tabby-check-availability/',
25
+ method: 'POST',
26
+ body
27
+ }),
28
+ async onQueryStarted(arg, { dispatch, queryFulfilled }) {
29
+ dispatch(setPaymentStepBusy(true));
30
+ await queryFulfilled;
31
+ dispatch(setPaymentStepBusy(false));
32
+ }
33
+ })
34
+ })
35
+ });
36
+
37
+ export const { useCheckTabbyAvailabilityMutation } = tabbyApi;
@@ -14,28 +14,24 @@ export const getRandomString = (length: number): string => {
14
14
  ).join('');
15
15
  };
16
16
 
17
- export const generateHash = (
18
- hashKey: string,
19
- sessionId: string,
20
- salt: string
21
- ): string => {
22
- const hashStr = `${salt}|${sessionId}|${hashKey}`;
17
+ export const generateHash = (...values: string[]): string => {
18
+ const hashStr = values.join('|');
23
19
  return crypto.createHash('sha512').update(hashStr, 'utf8').digest('hex');
24
20
  };
25
21
 
26
22
  export const groupByProductId = (orderItems: any[]) => {
27
23
  return Object.values(
28
- orderItems.reduce((acc, item) => {
29
- const productId = item.product.pk;
24
+ orderItems?.reduce((acc, item) => {
25
+ const productId = item?.product?.pk;
30
26
  acc[productId] = acc[productId] || {
31
- unit_price: item.price,
32
- title: item.product.name,
27
+ unit_price: item?.price,
28
+ title: item?.product?.name,
33
29
  quantity: 0,
34
- category: item.product.category.name
30
+ category: item?.product?.category?.name
35
31
  };
36
32
  acc[productId].quantity += 1;
37
33
  return acc;
38
- }, {})
34
+ }, {}) || {}
39
35
  );
40
36
  };
41
37
 
@@ -64,3 +60,13 @@ export const getOrderStatus = (
64
60
  return OrderServiceStatus.Unknown;
65
61
  }
66
62
  };
63
+
64
+ export const getISODateWithMicroseconds = () => {
65
+ const now = new Date();
66
+ const isoString = now.toISOString();
67
+
68
+ const milliseconds = now.getMilliseconds().toString().padStart(3, '0');
69
+ const microseconds = milliseconds + '650';
70
+
71
+ return isoString.replace(/\.\d{3}Z$/, `.${microseconds}Z`);
72
+ };