strapi-plugin-payone-provider 1.2.4 â 1.3.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/README.md +112 -0
- package/admin/src/pages/App/components/AppTabs.js +8 -0
- package/admin/src/pages/App/components/GooglePaybutton.js +7 -7
- package/admin/src/pages/App/components/PaymentActionsPanel.js +25 -1
- package/admin/src/pages/App/components/StatusBadge.js +3 -1
- package/admin/src/pages/App/components/paymentActions/AuthorizationForm.js +34 -5
- package/admin/src/pages/App/components/paymentActions/CardDetailsInput.js +189 -0
- package/admin/src/pages/App/components/paymentActions/PaymentResult.js +68 -5
- package/admin/src/pages/App/components/paymentActions/PreauthorizationForm.js +34 -5
- package/admin/src/pages/hooks/usePaymentActions.js +200 -15
- package/admin/src/pages/utils/paymentUtils.js +6 -2
- package/package.json +1 -1
- package/server/services/paymentService.js +62 -5
- package/server/utils/requestBuilder.js +11 -0
- package/server/utils/responseParser.js +59 -5
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A comprehensive Strapi plugin that integrates the Payone payment gateway into yo
|
|
|
10
10
|
- [Configuration](#configuration)
|
|
11
11
|
- [Getting Started](#getting-started)
|
|
12
12
|
- [Usage](#usage)
|
|
13
|
+
- [3D Secure (3DS) Authentication](#-3d-secure-3ds-authentication)
|
|
13
14
|
- [Payment Methods & Operations](#-payment-methods--operations)
|
|
14
15
|
- [Supported Payment Methods](#supported-payment-methods)
|
|
15
16
|
|
|
@@ -136,6 +137,117 @@ All responses include:
|
|
|
136
137
|
|
|
137
138
|
---
|
|
138
139
|
|
|
140
|
+
## đ 3D Secure (3DS) Authentication
|
|
141
|
+
|
|
142
|
+
3D Secure (3DS) is a security protocol that adds an extra layer of authentication for credit card payments, ensuring compliance with Strong Customer Authentication (SCA) requirements.
|
|
143
|
+
|
|
144
|
+
### Enabling 3D Secure
|
|
145
|
+
|
|
146
|
+
1. Navigate to **Payone Provider** in the Strapi admin panel
|
|
147
|
+
2. Go to the **Configuration** tab
|
|
148
|
+
3. Find the **"Enable 3D Secure"** dropdown
|
|
149
|
+
4. Select **"Enabled"** to activate 3DS for credit card payments
|
|
150
|
+
5. Click **"Save Configuration"**
|
|
151
|
+
|
|
152
|
+
> â ī¸ **Note**: When 3DS is enabled, it only applies to **credit card** payments (`clearingtype: "cc"`). Other payment methods are not affected.
|
|
153
|
+
|
|
154
|
+
### Supported Operations
|
|
155
|
+
|
|
156
|
+
3D Secure works with the following operations:
|
|
157
|
+
|
|
158
|
+
- â
**Preauthorization** (`POST /api/strapi-plugin-payone-provider/preauthorization`)
|
|
159
|
+
- â
**Authorization** (`POST /api/strapi-plugin-payone-provider/authorization`)
|
|
160
|
+
- â **Capture** - Not applicable (uses preauthorized transaction)
|
|
161
|
+
- â **Refund** - Not applicable (uses existing transaction)
|
|
162
|
+
|
|
163
|
+
### Required Parameters for Preauthorization/Authorization with 3DS
|
|
164
|
+
|
|
165
|
+
When 3DS is enabled and you're making a credit card payment, the following parameters are required:
|
|
166
|
+
|
|
167
|
+
**Credit Card Details** (required when 3DS is enabled):
|
|
168
|
+
|
|
169
|
+
- `cardtype`: Card type (`"V"` for VISA, `"M"` for Mastercard, `"A"` for AMEX, etc.)
|
|
170
|
+
- `cardpan`: Card number (PAN)
|
|
171
|
+
- `cardexpiredate`: Expiry date in format `YYMM` (e.g., `"2512"` for December 2025)
|
|
172
|
+
- `cardcvc2`: CVC/CVV code (3 digits for most cards, 4 digits for AMEX)
|
|
173
|
+
|
|
174
|
+
**Redirect URLs** (required for 3DS authentication flow):
|
|
175
|
+
|
|
176
|
+
- `successurl`: URL to redirect after successful 3DS authentication
|
|
177
|
+
- `errorurl`: URL to redirect after 3DS authentication error
|
|
178
|
+
- `backurl`: URL to redirect if user cancels 3DS authentication
|
|
179
|
+
|
|
180
|
+
**Example Request**:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"amount": 1000,
|
|
185
|
+
"currency": "EUR",
|
|
186
|
+
"reference": "PAY1234567890ABCDEF",
|
|
187
|
+
"clearingtype": "cc",
|
|
188
|
+
"cardtype": "V",
|
|
189
|
+
"cardpan": "4111111111111111",
|
|
190
|
+
"cardexpiredate": "2512",
|
|
191
|
+
"cardcvc2": "123",
|
|
192
|
+
"firstname": "John",
|
|
193
|
+
"lastname": "Doe",
|
|
194
|
+
"email": "john.doe@example.com",
|
|
195
|
+
"street": "Main Street 123",
|
|
196
|
+
"zip": "12345",
|
|
197
|
+
"city": "Berlin",
|
|
198
|
+
"country": "DE",
|
|
199
|
+
"successurl": "https://www.example.com/success",
|
|
200
|
+
"errorurl": "https://www.example.com/error",
|
|
201
|
+
"backurl": "https://www.example.com/back"
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 3DS Response Handling
|
|
206
|
+
|
|
207
|
+
When 3DS is required, the API response will include:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"data": {
|
|
212
|
+
"status": "REDIRECT",
|
|
213
|
+
"redirecturl": "https://secure.pay1.de/3ds/...",
|
|
214
|
+
"requires3DSRedirect": true,
|
|
215
|
+
"txid": "123456789"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Response Fields**:
|
|
221
|
+
|
|
222
|
+
- `status`: `"REDIRECT"` when 3DS authentication is required
|
|
223
|
+
- `redirecturl`: URL to redirect the customer for 3DS authentication
|
|
224
|
+
- `requires3DSRedirect`: Boolean indicating if redirect is needed
|
|
225
|
+
- `txid`: Transaction ID (if available)
|
|
226
|
+
|
|
227
|
+
### 3DS Callback Endpoint
|
|
228
|
+
|
|
229
|
+
After the customer completes 3DS authentication, Payone will send a callback to:
|
|
230
|
+
|
|
231
|
+
**URL**: `POST /api/strapi-plugin-payone-provider/3ds-callback`
|
|
232
|
+
|
|
233
|
+
This endpoint processes the 3DS authentication result and updates the transaction status.
|
|
234
|
+
|
|
235
|
+
> âšī¸ **Note**: The callback endpoint is automatically handled by the plugin. You don't need to manually process it unless you're implementing custom callback handling.
|
|
236
|
+
|
|
237
|
+
### How It Works
|
|
238
|
+
|
|
239
|
+
1. **Request**: Send a preauthorization or authorization request with credit card details and redirect URLs
|
|
240
|
+
2. **Response**: If 3DS is required, you'll receive a `REDIRECT` status with a `redirecturl`
|
|
241
|
+
3. **Redirect**: Redirect the customer to the `redirecturl` for 3DS authentication
|
|
242
|
+
4. **Callback**: After authentication, Payone redirects back to your `successurl`, `errorurl`, or `backurl` with transaction data
|
|
243
|
+
5. **Completion**: The transaction is completed based on the authentication result
|
|
244
|
+
|
|
245
|
+
### Testing 3DS
|
|
246
|
+
|
|
247
|
+
For testing 3DS authentication, use test cards that trigger 3DS challenges. Refer to the [Payone 3D Secure Documentation](https://docs.payone.com/security-risk-management/3d-secure#/) for test card numbers and scenarios.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
139
251
|
## đŗ Payment Methods & Operations
|
|
140
252
|
|
|
141
253
|
### Credit Card
|
|
@@ -115,6 +115,14 @@ const AppTabs = ({
|
|
|
115
115
|
settings={settings}
|
|
116
116
|
googlePayToken={paymentActions.googlePayToken}
|
|
117
117
|
setGooglePayToken={paymentActions.setGooglePayToken}
|
|
118
|
+
cardtype={paymentActions.cardtype}
|
|
119
|
+
setCardtype={paymentActions.setCardtype}
|
|
120
|
+
cardpan={paymentActions.cardpan}
|
|
121
|
+
setCardpan={paymentActions.setCardpan}
|
|
122
|
+
cardexpiredate={paymentActions.cardexpiredate}
|
|
123
|
+
setCardexpiredate={paymentActions.setCardexpiredate}
|
|
124
|
+
cardcvc2={paymentActions.cardcvc2}
|
|
125
|
+
setCardcvc2={paymentActions.setCardcvc2}
|
|
118
126
|
/>
|
|
119
127
|
</TabPanel>
|
|
120
128
|
</TabPanels>
|
|
@@ -269,29 +269,29 @@ const GooglePayButton = ({
|
|
|
269
269
|
};
|
|
270
270
|
|
|
271
271
|
return (
|
|
272
|
-
<Box
|
|
273
|
-
<Flex direction="column" gap={3}>
|
|
272
|
+
<Box width="100%">
|
|
273
|
+
<Flex direction="column" gap={3} alignItems="stretch">
|
|
274
274
|
{isLoading && (
|
|
275
|
-
<Typography variant="pi" textColor="neutral600">
|
|
275
|
+
<Typography variant="pi" textColor="neutral600" style={{ textAlign: "left" }}>
|
|
276
276
|
Loading Google Pay...
|
|
277
277
|
</Typography>
|
|
278
278
|
)}
|
|
279
279
|
{!isLoading && !isReady && (
|
|
280
|
-
<Typography variant="pi" textColor="neutral600">
|
|
280
|
+
<Typography variant="pi" textColor="neutral600" style={{ textAlign: "left" }}>
|
|
281
281
|
Google Pay is not available
|
|
282
282
|
</Typography>
|
|
283
283
|
)}
|
|
284
284
|
{!isLoading && isReady && (
|
|
285
285
|
<>
|
|
286
|
-
<Typography variant="sigma" textColor="neutral700" fontWeight="semiBold">
|
|
286
|
+
<Typography variant="sigma" textColor="neutral700" fontWeight="semiBold" style={{ textAlign: "left" }}>
|
|
287
287
|
Google Pay Payment
|
|
288
288
|
</Typography>
|
|
289
|
-
<Typography variant="pi" textColor="neutral600">
|
|
289
|
+
<Typography variant="pi" textColor="neutral600" style={{ textAlign: "left" }}>
|
|
290
290
|
Click the button below to pay with Google Pay. The token will be automatically sent to Payone.
|
|
291
291
|
</Typography>
|
|
292
292
|
</>
|
|
293
293
|
)}
|
|
294
|
-
<Box ref={buttonContainerRef} style={{ minHeight: "40px" }} />
|
|
294
|
+
<Box ref={buttonContainerRef} style={{ minHeight: "40px", width: "100%", display: "flex", justifyContent: "flex-start" }} />
|
|
295
295
|
</Flex>
|
|
296
296
|
</Box>
|
|
297
297
|
);
|
|
@@ -35,7 +35,15 @@ const PaymentActionsPanel = ({
|
|
|
35
35
|
onRefund,
|
|
36
36
|
settings,
|
|
37
37
|
googlePayToken,
|
|
38
|
-
setGooglePayToken
|
|
38
|
+
setGooglePayToken,
|
|
39
|
+
cardtype,
|
|
40
|
+
setCardtype,
|
|
41
|
+
cardpan,
|
|
42
|
+
setCardpan,
|
|
43
|
+
cardexpiredate,
|
|
44
|
+
setCardexpiredate,
|
|
45
|
+
cardcvc2,
|
|
46
|
+
setCardcvc2
|
|
39
47
|
}) => {
|
|
40
48
|
return (
|
|
41
49
|
<Box
|
|
@@ -76,6 +84,14 @@ const PaymentActionsPanel = ({
|
|
|
76
84
|
settings={settings}
|
|
77
85
|
googlePayToken={googlePayToken}
|
|
78
86
|
setGooglePayToken={setGooglePayToken}
|
|
87
|
+
cardtype={cardtype}
|
|
88
|
+
setCardtype={setCardtype}
|
|
89
|
+
cardpan={cardpan}
|
|
90
|
+
setCardpan={setCardpan}
|
|
91
|
+
cardexpiredate={cardexpiredate}
|
|
92
|
+
setCardexpiredate={setCardexpiredate}
|
|
93
|
+
cardcvc2={cardcvc2}
|
|
94
|
+
setCardcvc2={setCardcvc2}
|
|
79
95
|
/>
|
|
80
96
|
</Box>
|
|
81
97
|
|
|
@@ -93,6 +109,14 @@ const PaymentActionsPanel = ({
|
|
|
93
109
|
settings={settings}
|
|
94
110
|
googlePayToken={googlePayToken}
|
|
95
111
|
setGooglePayToken={setGooglePayToken}
|
|
112
|
+
cardtype={cardtype}
|
|
113
|
+
setCardtype={setCardtype}
|
|
114
|
+
cardpan={cardpan}
|
|
115
|
+
setCardpan={setCardpan}
|
|
116
|
+
cardexpiredate={cardexpiredate}
|
|
117
|
+
setCardexpiredate={setCardexpiredate}
|
|
118
|
+
cardcvc2={cardcvc2}
|
|
119
|
+
setCardcvc2={setCardcvc2}
|
|
96
120
|
/>
|
|
97
121
|
</Box>
|
|
98
122
|
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { Box, Flex, Typography, TextInput, Button } from "@strapi/design-system";
|
|
3
3
|
import { Play } from "@strapi/icons";
|
|
4
4
|
import GooglePayButton from "../GooglePaybutton";
|
|
5
|
+
import CardDetailsInput from "./CardDetailsInput";
|
|
5
6
|
|
|
6
7
|
const AuthorizationForm = ({
|
|
7
8
|
paymentAmount,
|
|
@@ -13,7 +14,15 @@ const AuthorizationForm = ({
|
|
|
13
14
|
paymentMethod,
|
|
14
15
|
settings,
|
|
15
16
|
googlePayToken,
|
|
16
|
-
setGooglePayToken
|
|
17
|
+
setGooglePayToken,
|
|
18
|
+
cardtype,
|
|
19
|
+
setCardtype,
|
|
20
|
+
cardpan,
|
|
21
|
+
setCardpan,
|
|
22
|
+
cardexpiredate,
|
|
23
|
+
setCardexpiredate,
|
|
24
|
+
cardcvc2,
|
|
25
|
+
setCardcvc2
|
|
17
26
|
}) => {
|
|
18
27
|
const handleGooglePayToken = (token, paymentData) => {
|
|
19
28
|
if (!token) {
|
|
@@ -57,14 +66,29 @@ const AuthorizationForm = ({
|
|
|
57
66
|
name="authReference"
|
|
58
67
|
value={authReference}
|
|
59
68
|
onChange={(e) => setAuthReference(e.target.value)}
|
|
60
|
-
placeholder="
|
|
61
|
-
hint="Reference
|
|
62
|
-
required
|
|
69
|
+
placeholder="Auto-generated if empty"
|
|
70
|
+
hint="Reference will be auto-generated if left empty"
|
|
63
71
|
className="payment-input"
|
|
64
72
|
style={{ flex: 1, minWidth: "250px" }}
|
|
65
73
|
/>
|
|
66
74
|
</Flex>
|
|
67
75
|
|
|
76
|
+
{/* Show card details input if 3DS is enabled and payment method is credit card */}
|
|
77
|
+
{paymentMethod === "cc" && settings?.enable3DSecure !== false && (
|
|
78
|
+
<Box marginTop={4}>
|
|
79
|
+
<CardDetailsInput
|
|
80
|
+
cardtype={cardtype}
|
|
81
|
+
setCardtype={setCardtype}
|
|
82
|
+
cardpan={cardpan}
|
|
83
|
+
setCardpan={setCardpan}
|
|
84
|
+
cardexpiredate={cardexpiredate}
|
|
85
|
+
setCardexpiredate={setCardexpiredate}
|
|
86
|
+
cardcvc2={cardcvc2}
|
|
87
|
+
setCardcvc2={setCardcvc2}
|
|
88
|
+
/>
|
|
89
|
+
</Box>
|
|
90
|
+
)}
|
|
91
|
+
|
|
68
92
|
{paymentMethod === "gpp" ? (
|
|
69
93
|
<GooglePayButton
|
|
70
94
|
amount={paymentAmount}
|
|
@@ -80,7 +104,12 @@ const AuthorizationForm = ({
|
|
|
80
104
|
loading={isProcessingPayment}
|
|
81
105
|
startIcon={<Play />}
|
|
82
106
|
className="payment-button payment-button-primary"
|
|
83
|
-
disabled={
|
|
107
|
+
disabled={
|
|
108
|
+
!paymentAmount.trim() ||
|
|
109
|
+
(paymentMethod === "cc" &&
|
|
110
|
+
settings?.enable3DSecure !== false &&
|
|
111
|
+
(!cardtype || !cardpan || !cardexpiredate || !cardcvc2))
|
|
112
|
+
}
|
|
84
113
|
>
|
|
85
114
|
Process Authorization
|
|
86
115
|
</Button>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Flex,
|
|
5
|
+
Typography,
|
|
6
|
+
TextInput,
|
|
7
|
+
Select,
|
|
8
|
+
Option,
|
|
9
|
+
Link
|
|
10
|
+
} from "@strapi/design-system";
|
|
11
|
+
|
|
12
|
+
// 3DS Test Cards that require redirect (challenge workflow)
|
|
13
|
+
const TEST_3DS_CARDS = [
|
|
14
|
+
{
|
|
15
|
+
name: "VISA - 3DS 2.0 (Challenge)",
|
|
16
|
+
cardtype: "V",
|
|
17
|
+
cardpan: "4716971940353559",
|
|
18
|
+
cardexpiredate: "2512",
|
|
19
|
+
cardcvc2: "123",
|
|
20
|
+
description: "3DS 2.0 with challenge - Password: 12345"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "Mastercard - 3DS 2.0 (Challenge)",
|
|
24
|
+
cardtype: "M",
|
|
25
|
+
cardpan: "5404127720739582",
|
|
26
|
+
cardexpiredate: "2512",
|
|
27
|
+
cardcvc2: "123",
|
|
28
|
+
description: "3DS 2.0 with challenge - Password: 12345"
|
|
29
|
+
},
|
|
30
|
+
// {
|
|
31
|
+
// name: "AMEX - 3DS 2.0 (Challenge)",
|
|
32
|
+
// cardtype: "A",
|
|
33
|
+
// cardpan: "375144726036141",
|
|
34
|
+
// cardexpiredate: "2512",
|
|
35
|
+
// cardcvc2: "1234",
|
|
36
|
+
// description: "3DS 2.0 with challenge - Password: 12345"
|
|
37
|
+
// }
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const CardDetailsInput = ({
|
|
41
|
+
cardtype,
|
|
42
|
+
setCardtype,
|
|
43
|
+
cardpan,
|
|
44
|
+
setCardpan,
|
|
45
|
+
cardexpiredate,
|
|
46
|
+
setCardexpiredate,
|
|
47
|
+
cardcvc2,
|
|
48
|
+
setCardcvc2
|
|
49
|
+
}) => {
|
|
50
|
+
const [selectedTestCard, setSelectedTestCard] = React.useState("");
|
|
51
|
+
const isUpdatingFromTestCard = useRef(false);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (isUpdatingFromTestCard.current) {
|
|
55
|
+
isUpdatingFromTestCard.current = false;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const matchingCard = TEST_3DS_CARDS.find(
|
|
60
|
+
card => card.cardtype === cardtype && card.cardpan === cardpan
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (matchingCard) {
|
|
64
|
+
const testCardValue = `${matchingCard.cardtype}-${matchingCard.cardpan}`;
|
|
65
|
+
if (selectedTestCard !== testCardValue) {
|
|
66
|
+
setSelectedTestCard(testCardValue);
|
|
67
|
+
}
|
|
68
|
+
} else if (selectedTestCard) {
|
|
69
|
+
setSelectedTestCard("");
|
|
70
|
+
}
|
|
71
|
+
}, [cardtype, cardpan, selectedTestCard]);
|
|
72
|
+
|
|
73
|
+
const handleTestCardSelect = (value) => {
|
|
74
|
+
if (!value || value === "") {
|
|
75
|
+
setSelectedTestCard("");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const selectedCard = TEST_3DS_CARDS.find(card =>
|
|
80
|
+
`${card.cardtype}-${card.cardpan}` === value
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (selectedCard) {
|
|
84
|
+
isUpdatingFromTestCard.current = true;
|
|
85
|
+
|
|
86
|
+
setCardtype(selectedCard.cardtype);
|
|
87
|
+
setCardpan(selectedCard.cardpan);
|
|
88
|
+
setCardexpiredate(selectedCard.cardexpiredate);
|
|
89
|
+
setCardcvc2(selectedCard.cardcvc2);
|
|
90
|
+
setSelectedTestCard(value);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Box>
|
|
96
|
+
<Flex direction="column" alignItems="stretch" gap={4}>
|
|
97
|
+
<Select
|
|
98
|
+
label="3D Secure Test Cards (Requires Redirect)"
|
|
99
|
+
name="testCard"
|
|
100
|
+
value={selectedTestCard}
|
|
101
|
+
placeholder="Select a 3DS test card to auto-fill"
|
|
102
|
+
hint="These cards will trigger 3DS authentication redirect. Password: 12345"
|
|
103
|
+
onChange={handleTestCardSelect}
|
|
104
|
+
className="payment-input"
|
|
105
|
+
>
|
|
106
|
+
<Option value="">-- Select a test card --</Option>
|
|
107
|
+
{TEST_3DS_CARDS.map((card, index) => (
|
|
108
|
+
<Option key={index} value={`${card.cardtype}-${card.cardpan}`}>
|
|
109
|
+
{card.name} - {card.description}
|
|
110
|
+
</Option>
|
|
111
|
+
))}
|
|
112
|
+
</Select>
|
|
113
|
+
|
|
114
|
+
<Flex gap={4} wrap="wrap" alignItems="flex-start">
|
|
115
|
+
<Select
|
|
116
|
+
label="Card Type *"
|
|
117
|
+
name="cardtype"
|
|
118
|
+
value={cardtype || ""}
|
|
119
|
+
onChange={(value) => setCardtype(value)}
|
|
120
|
+
required
|
|
121
|
+
hint="Select credit card type"
|
|
122
|
+
className="payment-input"
|
|
123
|
+
style={{ flex: 1, minWidth: "200px" }}
|
|
124
|
+
>
|
|
125
|
+
<Option value="V">VISA</Option>
|
|
126
|
+
<Option value="M">Mastercard</Option>
|
|
127
|
+
<Option value="A">American Express</Option>
|
|
128
|
+
<Option value="J">JCB</Option>
|
|
129
|
+
<Option value="O">Maestro International</Option>
|
|
130
|
+
<Option value="D">Diners Club</Option>
|
|
131
|
+
</Select>
|
|
132
|
+
|
|
133
|
+
<TextInput
|
|
134
|
+
label="Card Number (PAN) *"
|
|
135
|
+
name="cardpan"
|
|
136
|
+
value={cardpan || ""}
|
|
137
|
+
onChange={(e) => setCardpan(e.target.value)}
|
|
138
|
+
placeholder="Enter card number"
|
|
139
|
+
hint="Credit card number (PAN)"
|
|
140
|
+
required
|
|
141
|
+
className="payment-input"
|
|
142
|
+
style={{ flex: 2, minWidth: "300px" }}
|
|
143
|
+
/>
|
|
144
|
+
</Flex>
|
|
145
|
+
|
|
146
|
+
<Flex gap={4} wrap="wrap" alignItems="flex-start">
|
|
147
|
+
<TextInput
|
|
148
|
+
label="Expiry Date *"
|
|
149
|
+
name="cardexpiredate"
|
|
150
|
+
value={cardexpiredate || ""}
|
|
151
|
+
onChange={(e) => setCardexpiredate(e.target.value)}
|
|
152
|
+
placeholder="YYMM (e.g., 2512)"
|
|
153
|
+
hint="Format: YYMM (e.g., 2512 = December 2025)"
|
|
154
|
+
required
|
|
155
|
+
maxLength={4}
|
|
156
|
+
className="payment-input"
|
|
157
|
+
style={{ flex: 1, minWidth: "150px" }}
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
<TextInput
|
|
161
|
+
label="CVC/CVV *"
|
|
162
|
+
name="cardcvc2"
|
|
163
|
+
value={cardcvc2 || ""}
|
|
164
|
+
onChange={(e) => setCardcvc2(e.target.value)}
|
|
165
|
+
placeholder="123 or 1234"
|
|
166
|
+
hint={cardtype === "A" ? "4 digits for AMEX" : "3 digits for other cards"}
|
|
167
|
+
required
|
|
168
|
+
maxLength={4}
|
|
169
|
+
className="payment-input"
|
|
170
|
+
style={{ flex: 1, minWidth: "150px" }}
|
|
171
|
+
/>
|
|
172
|
+
</Flex>
|
|
173
|
+
|
|
174
|
+
<Box paddingTop={2}>
|
|
175
|
+
<Typography variant="pi" textColor="neutral600" style={{ textAlign: "left" }}>
|
|
176
|
+
For all test card numbers (positive, negative, frictionless 3DS), 3D Secure test data, and detailed documentation, please refer to the{" "}
|
|
177
|
+
<Link href="https://docs.payone.com/security-risk-management/3d-secure#/" target="_blank" rel="noopener noreferrer">
|
|
178
|
+
Payone 3D Secure Documentation
|
|
179
|
+
</Link>
|
|
180
|
+
.
|
|
181
|
+
</Typography>
|
|
182
|
+
</Box>
|
|
183
|
+
</Flex>
|
|
184
|
+
</Box>
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export default CardDetailsInput;
|
|
189
|
+
|
|
@@ -17,11 +17,17 @@ const PaymentResult = ({ paymentError, paymentResult }) => {
|
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const status = paymentResult?.status || paymentResult?.Status || "";
|
|
21
|
+
const errorCode = paymentResult?.errorcode || paymentResult?.errorCode || paymentResult?.ErrorCode;
|
|
22
|
+
const errorMessage = paymentResult?.errormessage || paymentResult?.errorMessage || paymentResult?.ErrorMessage;
|
|
23
|
+
const customerMessage = paymentResult?.customermessage || paymentResult?.customerMessage || paymentResult?.CustomerMessage;
|
|
24
|
+
const isError = status === "ERROR" || status === "INVALID" || errorCode;
|
|
25
|
+
|
|
20
26
|
return (
|
|
21
27
|
<>
|
|
22
28
|
{paymentError && (
|
|
23
|
-
<Alert
|
|
24
|
-
variant="danger"
|
|
29
|
+
<Alert
|
|
30
|
+
variant="danger"
|
|
25
31
|
title="Error"
|
|
26
32
|
className="payment-alert"
|
|
27
33
|
>
|
|
@@ -37,14 +43,40 @@ const PaymentResult = ({ paymentError, paymentResult }) => {
|
|
|
37
43
|
<Typography variant="delta" as="h3" className="payment-section-title">
|
|
38
44
|
Payment Result
|
|
39
45
|
</Typography>
|
|
40
|
-
{paymentResult.Status && (
|
|
41
|
-
<StatusBadge status={paymentResult.Status} />
|
|
46
|
+
{(status || paymentResult.Status) && (
|
|
47
|
+
<StatusBadge status={status || paymentResult.Status} />
|
|
42
48
|
)}
|
|
43
49
|
</Flex>
|
|
44
50
|
|
|
45
51
|
<hr className="payment-divider" style={{ margin: '16px 0' }} />
|
|
46
52
|
|
|
53
|
+
{/* Show error information prominently if error */}
|
|
54
|
+
{isError && (
|
|
55
|
+
<Alert variant="danger" title="Transaction Failed">
|
|
56
|
+
<Stack spacing={2}>
|
|
57
|
+
{errorCode && (
|
|
58
|
+
<Typography variant="pi">
|
|
59
|
+
<strong>Error Code:</strong> {errorCode}
|
|
60
|
+
</Typography>
|
|
61
|
+
)}
|
|
62
|
+
{errorMessage && (
|
|
63
|
+
<Typography variant="pi">
|
|
64
|
+
<strong>Error Message:</strong> {errorMessage}
|
|
65
|
+
</Typography>
|
|
66
|
+
)}
|
|
67
|
+
{customerMessage && (
|
|
68
|
+
<Typography variant="pi">
|
|
69
|
+
<strong>Customer Message:</strong> {customerMessage}
|
|
70
|
+
</Typography>
|
|
71
|
+
)}
|
|
72
|
+
</Stack>
|
|
73
|
+
</Alert>
|
|
74
|
+
)}
|
|
75
|
+
|
|
47
76
|
<Box>
|
|
77
|
+
<Typography variant="omega" fontWeight="semiBold" marginBottom={2}>
|
|
78
|
+
Full Response Details:
|
|
79
|
+
</Typography>
|
|
48
80
|
<Stack spacing={3}>
|
|
49
81
|
{formatTransactionData(paymentResult).map((item, index) => (
|
|
50
82
|
<Flex
|
|
@@ -65,7 +97,14 @@ const PaymentResult = ({ paymentError, paymentResult }) => {
|
|
|
65
97
|
</Typography>
|
|
66
98
|
<Typography
|
|
67
99
|
variant="pi"
|
|
68
|
-
style={{
|
|
100
|
+
style={{
|
|
101
|
+
flex: 1,
|
|
102
|
+
textAlign: "right",
|
|
103
|
+
fontWeight: '400',
|
|
104
|
+
wordBreak: 'break-word',
|
|
105
|
+
fontFamily: item.key.toLowerCase().includes('raw') ? 'monospace' : 'inherit',
|
|
106
|
+
fontSize: item.key.toLowerCase().includes('raw') ? '11px' : 'inherit'
|
|
107
|
+
}}
|
|
69
108
|
>
|
|
70
109
|
{item.value}
|
|
71
110
|
</Typography>
|
|
@@ -73,6 +112,30 @@ const PaymentResult = ({ paymentError, paymentResult }) => {
|
|
|
73
112
|
))}
|
|
74
113
|
</Stack>
|
|
75
114
|
</Box>
|
|
115
|
+
|
|
116
|
+
{/* 3DS Required Warning */}
|
|
117
|
+
{paymentResult?.is3DSRequired && !paymentResult?.redirectUrl && (
|
|
118
|
+
<Alert variant="warning" title="3D Secure Authentication Required">
|
|
119
|
+
<Stack spacing={2}>
|
|
120
|
+
<Typography variant="pi">
|
|
121
|
+
Payone requires 3D Secure authentication, but no redirect URL was provided in the response.
|
|
122
|
+
</Typography>
|
|
123
|
+
<Typography variant="pi" fontWeight="semiBold">
|
|
124
|
+
Possible solutions:
|
|
125
|
+
</Typography>
|
|
126
|
+
<Typography variant="pi" component="ul" style={{ marginLeft: '20px' }}>
|
|
127
|
+
<li>Check Payone portal configuration for 3DS settings</li>
|
|
128
|
+
<li>Verify that redirect URLs (successurl, errorurl, backurl) are properly configured</li>
|
|
129
|
+
<li>Ensure you're using test mode with proper test credentials</li>
|
|
130
|
+
<li>Check if 3dscheck request is needed before authorization</li>
|
|
131
|
+
</Typography>
|
|
132
|
+
<Typography variant="pi" textColor="neutral600" marginTop={2}>
|
|
133
|
+
<strong>Error Code:</strong> {paymentResult?.errorCode || paymentResult?.ErrorCode || "4219"}
|
|
134
|
+
</Typography>
|
|
135
|
+
</Stack>
|
|
136
|
+
</Alert>
|
|
137
|
+
)}
|
|
138
|
+
|
|
76
139
|
</Stack>
|
|
77
140
|
</CardBody>
|
|
78
141
|
</Card>
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { Box, Flex, Typography, TextInput, Button } from "@strapi/design-system";
|
|
3
3
|
import { Play } from "@strapi/icons";
|
|
4
4
|
import GooglePayButton from "../GooglePaybutton";
|
|
5
|
+
import CardDetailsInput from "./CardDetailsInput";
|
|
5
6
|
|
|
6
7
|
const PreauthorizationForm = ({
|
|
7
8
|
paymentAmount,
|
|
@@ -13,7 +14,15 @@ const PreauthorizationForm = ({
|
|
|
13
14
|
paymentMethod,
|
|
14
15
|
settings,
|
|
15
16
|
googlePayToken,
|
|
16
|
-
setGooglePayToken
|
|
17
|
+
setGooglePayToken,
|
|
18
|
+
cardtype,
|
|
19
|
+
setCardtype,
|
|
20
|
+
cardpan,
|
|
21
|
+
setCardpan,
|
|
22
|
+
cardexpiredate,
|
|
23
|
+
setCardexpiredate,
|
|
24
|
+
cardcvc2,
|
|
25
|
+
setCardcvc2
|
|
17
26
|
}) => {
|
|
18
27
|
const handleGooglePayToken = (token, paymentData) => {
|
|
19
28
|
if (!token) {
|
|
@@ -57,14 +66,29 @@ const PreauthorizationForm = ({
|
|
|
57
66
|
name="preauthReference"
|
|
58
67
|
value={preauthReference}
|
|
59
68
|
onChange={(e) => setPreauthReference(e.target.value)}
|
|
60
|
-
placeholder="
|
|
61
|
-
hint="Reference
|
|
62
|
-
required
|
|
69
|
+
placeholder="Auto-generated if empty"
|
|
70
|
+
hint="Reference will be auto-generated if left empty"
|
|
63
71
|
className="payment-input"
|
|
64
72
|
style={{ flex: 1, minWidth: "250px" }}
|
|
65
73
|
/>
|
|
66
74
|
</Flex>
|
|
67
75
|
|
|
76
|
+
{/* Show card details input if 3DS is enabled and payment method is credit card */}
|
|
77
|
+
{paymentMethod === "cc" && settings?.enable3DSecure !== false && (
|
|
78
|
+
<Box marginTop={4}>
|
|
79
|
+
<CardDetailsInput
|
|
80
|
+
cardtype={cardtype}
|
|
81
|
+
setCardtype={setCardtype}
|
|
82
|
+
cardpan={cardpan}
|
|
83
|
+
setCardpan={setCardpan}
|
|
84
|
+
cardexpiredate={cardexpiredate}
|
|
85
|
+
setCardexpiredate={setCardexpiredate}
|
|
86
|
+
cardcvc2={cardcvc2}
|
|
87
|
+
setCardcvc2={setCardcvc2}
|
|
88
|
+
/>
|
|
89
|
+
</Box>
|
|
90
|
+
)}
|
|
91
|
+
|
|
68
92
|
{paymentMethod === "gpp" ? (
|
|
69
93
|
<GooglePayButton
|
|
70
94
|
amount={paymentAmount}
|
|
@@ -80,7 +104,12 @@ const PreauthorizationForm = ({
|
|
|
80
104
|
loading={isProcessingPayment}
|
|
81
105
|
startIcon={<Play />}
|
|
82
106
|
className="payment-button payment-button-primary"
|
|
83
|
-
disabled={
|
|
107
|
+
disabled={
|
|
108
|
+
!paymentAmount.trim() ||
|
|
109
|
+
(paymentMethod === "cc" &&
|
|
110
|
+
settings?.enable3DSecure !== false &&
|
|
111
|
+
(!cardtype || !cardpan || !cardexpiredate || !cardcvc2))
|
|
112
|
+
}
|
|
84
113
|
>
|
|
85
114
|
Process Preauthorization
|
|
86
115
|
</Button>
|
|
@@ -31,8 +31,16 @@ const usePaymentActions = () => {
|
|
|
31
31
|
|
|
32
32
|
// Payment form state
|
|
33
33
|
const [paymentAmount, setPaymentAmount] = useState("1000");
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
|
|
35
|
+
// Generate reference automatically
|
|
36
|
+
const generateReference = (prefix = "REF") => {
|
|
37
|
+
const timestamp = Date.now().toString(36).toUpperCase();
|
|
38
|
+
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
39
|
+
return `${prefix}-${timestamp}${random}`.slice(0, 20);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const [preauthReference, setPreauthReference] = useState(generateReference("PRE"));
|
|
43
|
+
const [authReference, setAuthReference] = useState(generateReference("AUTH"));
|
|
36
44
|
const [captureTxid, setCaptureTxid] = useState("");
|
|
37
45
|
const [refundTxid, setRefundTxid] = useState("");
|
|
38
46
|
const [refundSequenceNumber, setRefundSequenceNumber] = useState("2");
|
|
@@ -41,6 +49,12 @@ const usePaymentActions = () => {
|
|
|
41
49
|
const [captureMode, setCaptureMode] = useState("full");
|
|
42
50
|
const [googlePayToken, setGooglePayToken] = useState(null);
|
|
43
51
|
|
|
52
|
+
// Card details for 3DS testing
|
|
53
|
+
const [cardtype, setCardtype] = useState("");
|
|
54
|
+
const [cardpan, setCardpan] = useState("");
|
|
55
|
+
const [cardexpiredate, setCardexpiredate] = useState("");
|
|
56
|
+
const [cardcvc2, setCardcvc2] = useState("");
|
|
57
|
+
|
|
44
58
|
// Payment processing state
|
|
45
59
|
const [isProcessingPayment, setIsProcessingPayment] = useState(false);
|
|
46
60
|
const [paymentResult, setPaymentResult] = useState(null);
|
|
@@ -70,14 +84,32 @@ const usePaymentActions = () => {
|
|
|
70
84
|
setPaymentError(null);
|
|
71
85
|
setPaymentResult(null);
|
|
72
86
|
try {
|
|
87
|
+
// Auto-generate reference if empty
|
|
88
|
+
const finalPreauthReference = preauthReference.trim() || generateReference("PRE");
|
|
89
|
+
if (!preauthReference.trim()) {
|
|
90
|
+
setPreauthReference(finalPreauthReference);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Determine currency based on card type
|
|
94
|
+
// American Express typically requires USD, other cards use EUR
|
|
95
|
+
const currency = (paymentMethod === "cc" && cardtype === "A") ? "USD" : "EUR";
|
|
96
|
+
|
|
73
97
|
const baseParams = {
|
|
74
98
|
amount: parseInt(paymentAmount),
|
|
75
|
-
currency:
|
|
76
|
-
reference:
|
|
99
|
+
currency: currency,
|
|
100
|
+
reference: finalPreauthReference,
|
|
77
101
|
enable3DSecure: settings.enable3DSecure !== false,
|
|
78
102
|
...DEFAULT_PAYMENT_DATA
|
|
79
103
|
};
|
|
80
104
|
|
|
105
|
+
// Add card details if credit card payment and 3DS enabled
|
|
106
|
+
if (paymentMethod === "cc" && settings.enable3DSecure !== false) {
|
|
107
|
+
if (cardtype) baseParams.cardtype = cardtype;
|
|
108
|
+
if (cardpan) baseParams.cardpan = cardpan;
|
|
109
|
+
if (cardexpiredate) baseParams.cardexpiredate = cardexpiredate;
|
|
110
|
+
if (cardcvc2) baseParams.cardcvc2 = cardcvc2;
|
|
111
|
+
}
|
|
112
|
+
|
|
81
113
|
const needsRedirectUrls =
|
|
82
114
|
(paymentMethod === "cc" && settings.enable3DSecure !== false) ||
|
|
83
115
|
["wlt", "gpp", "apl", "sb"].includes(paymentMethod);
|
|
@@ -100,19 +132,81 @@ const usePaymentActions = () => {
|
|
|
100
132
|
const result = await payoneRequests.preauthorization(params);
|
|
101
133
|
const responseData = result?.data || result;
|
|
102
134
|
|
|
135
|
+
// Log full response
|
|
136
|
+
console.log("Preauthorization Response:", responseData);
|
|
137
|
+
console.log("Response Status:", responseData.status || responseData.Status);
|
|
138
|
+
console.log("Response Error Code:", responseData.errorcode || responseData.errorCode || responseData.ErrorCode);
|
|
139
|
+
console.log("Response Error Message:", responseData.errormessage || responseData.errorMessage || responseData.ErrorMessage);
|
|
140
|
+
console.log("All redirect URL fields:", {
|
|
141
|
+
redirectUrl: responseData.redirectUrl,
|
|
142
|
+
redirecturl: responseData.redirecturl,
|
|
143
|
+
RedirectUrl: responseData.RedirectUrl,
|
|
144
|
+
redirect_url: responseData.redirect_url,
|
|
145
|
+
url: responseData.url,
|
|
146
|
+
Url: responseData.Url
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const status = (responseData.status || responseData.Status || "").toUpperCase();
|
|
150
|
+
const errorCode = responseData.errorcode || responseData.errorCode || responseData.ErrorCode;
|
|
151
|
+
const errorMessage = responseData.errormessage || responseData.errorMessage || responseData.ErrorMessage;
|
|
152
|
+
|
|
153
|
+
// Check for 3DS required error (4219)
|
|
154
|
+
const requires3DSErrorCodes = ["4219", 4219];
|
|
155
|
+
const is3DSRequiredError = requires3DSErrorCodes.includes(errorCode);
|
|
156
|
+
|
|
157
|
+
// Check all possible redirect URL fields
|
|
158
|
+
const redirectUrl =
|
|
159
|
+
responseData.redirectUrl ||
|
|
160
|
+
responseData.redirecturl ||
|
|
161
|
+
responseData.RedirectUrl ||
|
|
162
|
+
responseData.redirect_url ||
|
|
163
|
+
responseData.url ||
|
|
164
|
+
responseData.Url ||
|
|
165
|
+
null;
|
|
166
|
+
|
|
167
|
+
// If 3DS required but no redirect URL, show helpful message
|
|
168
|
+
if (is3DSRequiredError && !redirectUrl) {
|
|
169
|
+
console.warn("3DS authentication required (Error 4219) but no redirect URL found in response");
|
|
170
|
+
console.log("Full response:", JSON.stringify(responseData, null, 2));
|
|
171
|
+
setPaymentError(
|
|
172
|
+
"3D Secure authentication required. Please check Payone configuration and ensure redirect URLs are properly set. Error: " +
|
|
173
|
+
(errorMessage || `Error code: ${errorCode}`)
|
|
174
|
+
);
|
|
175
|
+
setPaymentResult(responseData);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for other errors (but not 3DS required)
|
|
180
|
+
if ((status === "ERROR" || status === "INVALID" || errorCode) && !is3DSRequiredError) {
|
|
181
|
+
setPaymentError(
|
|
182
|
+
errorMessage ||
|
|
183
|
+
`Payment failed with error code: ${errorCode || "Unknown"}` ||
|
|
184
|
+
"Preauthorization failed"
|
|
185
|
+
);
|
|
186
|
+
setPaymentResult(responseData);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
103
189
|
|
|
104
|
-
const redirectUrl = responseData.redirectUrl || responseData.redirecturl || responseData.RedirectUrl;
|
|
105
190
|
const needsRedirect = responseData.requires3DSRedirect ||
|
|
106
|
-
(
|
|
107
|
-
(
|
|
191
|
+
(status === "REDIRECT" && redirectUrl) ||
|
|
192
|
+
(is3DSRequiredError && redirectUrl);
|
|
108
193
|
|
|
109
194
|
if (needsRedirect && redirectUrl) {
|
|
195
|
+
console.log("Redirecting to 3DS:", redirectUrl);
|
|
110
196
|
window.location.href = redirectUrl;
|
|
111
197
|
return;
|
|
112
198
|
}
|
|
113
199
|
|
|
114
200
|
setPaymentResult(responseData);
|
|
115
|
-
|
|
201
|
+
|
|
202
|
+
if (status === "APPROVED") {
|
|
203
|
+
handlePaymentSuccess("Preauthorization completed successfully");
|
|
204
|
+
} else {
|
|
205
|
+
handlePaymentError(
|
|
206
|
+
{ message: `Unexpected status: ${status}` },
|
|
207
|
+
`Preauthorization completed with status: ${status}`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
116
210
|
} catch (error) {
|
|
117
211
|
handlePaymentError(error, "Preauthorization failed");
|
|
118
212
|
} finally {
|
|
@@ -126,14 +220,32 @@ const usePaymentActions = () => {
|
|
|
126
220
|
setPaymentResult(null);
|
|
127
221
|
|
|
128
222
|
try {
|
|
223
|
+
// Auto-generate reference if empty
|
|
224
|
+
const finalAuthReference = authReference.trim() || generateReference("AUTH");
|
|
225
|
+
if (!authReference.trim()) {
|
|
226
|
+
setAuthReference(finalAuthReference);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Determine currency based on card type
|
|
230
|
+
// American Express typically requires USD, other cards use EUR
|
|
231
|
+
const currency = (paymentMethod === "cc" && cardtype === "A") ? "USD" : "EUR";
|
|
232
|
+
|
|
129
233
|
const baseParams = {
|
|
130
234
|
amount: parseInt(paymentAmount),
|
|
131
|
-
currency:
|
|
132
|
-
reference:
|
|
235
|
+
currency: currency,
|
|
236
|
+
reference: finalAuthReference,
|
|
133
237
|
enable3DSecure: settings.enable3DSecure !== false,
|
|
134
238
|
...DEFAULT_PAYMENT_DATA
|
|
135
239
|
};
|
|
136
240
|
|
|
241
|
+
// Add card details if credit card payment and 3DS enabled
|
|
242
|
+
if (paymentMethod === "cc" && settings.enable3DSecure !== false) {
|
|
243
|
+
if (cardtype) baseParams.cardtype = cardtype;
|
|
244
|
+
if (cardpan) baseParams.cardpan = cardpan;
|
|
245
|
+
if (cardexpiredate) baseParams.cardexpiredate = cardexpiredate;
|
|
246
|
+
if (cardcvc2) baseParams.cardcvc2 = cardcvc2;
|
|
247
|
+
}
|
|
248
|
+
|
|
137
249
|
const needsRedirectUrls =
|
|
138
250
|
(paymentMethod === "cc" && settings.enable3DSecure !== false) ||
|
|
139
251
|
["wlt", "gpp", "apl", "sb"].includes(paymentMethod);
|
|
@@ -156,18 +268,81 @@ const usePaymentActions = () => {
|
|
|
156
268
|
const result = await payoneRequests.authorization(params);
|
|
157
269
|
const responseData = result?.data || result;
|
|
158
270
|
|
|
159
|
-
|
|
271
|
+
// Log full response
|
|
272
|
+
console.log("Authorization Response:", responseData);
|
|
273
|
+
console.log("Response Status:", responseData.status || responseData.Status);
|
|
274
|
+
console.log("Response Error Code:", responseData.errorcode || responseData.errorCode || responseData.ErrorCode);
|
|
275
|
+
console.log("Response Error Message:", responseData.errormessage || responseData.errorMessage || responseData.ErrorMessage);
|
|
276
|
+
console.log("All redirect URL fields:", {
|
|
277
|
+
redirectUrl: responseData.redirectUrl,
|
|
278
|
+
redirecturl: responseData.redirecturl,
|
|
279
|
+
RedirectUrl: responseData.RedirectUrl,
|
|
280
|
+
redirect_url: responseData.redirect_url,
|
|
281
|
+
url: responseData.url,
|
|
282
|
+
Url: responseData.Url
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const status = (responseData.status || responseData.Status || "").toUpperCase();
|
|
286
|
+
const errorCode = responseData.errorcode || responseData.errorCode || responseData.ErrorCode;
|
|
287
|
+
const errorMessage = responseData.errormessage || responseData.errorMessage || responseData.ErrorMessage;
|
|
288
|
+
|
|
289
|
+
// Check for 3DS required error (4219)
|
|
290
|
+
const requires3DSErrorCodes = ["4219", 4219];
|
|
291
|
+
const is3DSRequiredError = requires3DSErrorCodes.includes(errorCode);
|
|
292
|
+
|
|
293
|
+
// Check all possible redirect URL fields
|
|
294
|
+
const redirectUrl =
|
|
295
|
+
responseData.redirectUrl ||
|
|
296
|
+
responseData.redirecturl ||
|
|
297
|
+
responseData.RedirectUrl ||
|
|
298
|
+
responseData.redirect_url ||
|
|
299
|
+
responseData.url ||
|
|
300
|
+
responseData.Url ||
|
|
301
|
+
null;
|
|
302
|
+
|
|
303
|
+
// If 3DS required but no redirect URL, show helpful message
|
|
304
|
+
if (is3DSRequiredError && !redirectUrl) {
|
|
305
|
+
console.warn("3DS authentication required (Error 4219) but no redirect URL found in response");
|
|
306
|
+
console.log("Full response:", JSON.stringify(responseData, null, 2));
|
|
307
|
+
setPaymentError(
|
|
308
|
+
"3D Secure authentication required. Please check Payone configuration and ensure redirect URLs are properly set. Error: " +
|
|
309
|
+
(errorMessage || `Error code: ${errorCode}`)
|
|
310
|
+
);
|
|
311
|
+
setPaymentResult(responseData);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check for other errors (but not 3DS required)
|
|
316
|
+
if ((status === "ERROR" || status === "INVALID" || errorCode) && !is3DSRequiredError) {
|
|
317
|
+
setPaymentError(
|
|
318
|
+
errorMessage ||
|
|
319
|
+
`Payment failed with error code: ${errorCode || "Unknown"}` ||
|
|
320
|
+
"Authorization failed"
|
|
321
|
+
);
|
|
322
|
+
setPaymentResult(responseData);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
160
326
|
const needsRedirect = responseData.requires3DSRedirect ||
|
|
161
|
-
(
|
|
162
|
-
(
|
|
327
|
+
(status === "REDIRECT" && redirectUrl) ||
|
|
328
|
+
(is3DSRequiredError && redirectUrl);
|
|
163
329
|
|
|
164
330
|
if (needsRedirect && redirectUrl) {
|
|
331
|
+
console.log("Redirecting to 3DS:", redirectUrl);
|
|
165
332
|
window.location.href = redirectUrl;
|
|
166
333
|
return;
|
|
167
334
|
}
|
|
168
335
|
|
|
169
336
|
setPaymentResult(responseData);
|
|
170
|
-
|
|
337
|
+
|
|
338
|
+
if (status === "APPROVED") {
|
|
339
|
+
handlePaymentSuccess("Authorization completed successfully");
|
|
340
|
+
} else {
|
|
341
|
+
handlePaymentError(
|
|
342
|
+
{ message: `Unexpected status: ${status}` },
|
|
343
|
+
`Authorization completed with status: ${status}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
171
346
|
} catch (error) {
|
|
172
347
|
handlePaymentError(error, "Authorization failed");
|
|
173
348
|
} finally {
|
|
@@ -263,7 +438,17 @@ const usePaymentActions = () => {
|
|
|
263
438
|
|
|
264
439
|
// Google Pay
|
|
265
440
|
googlePayToken,
|
|
266
|
-
setGooglePayToken
|
|
441
|
+
setGooglePayToken,
|
|
442
|
+
|
|
443
|
+
// Card details for 3DS
|
|
444
|
+
cardtype,
|
|
445
|
+
setCardtype,
|
|
446
|
+
cardpan,
|
|
447
|
+
setCardpan,
|
|
448
|
+
cardexpiredate,
|
|
449
|
+
setCardexpiredate,
|
|
450
|
+
cardcvc2,
|
|
451
|
+
setCardcvc2
|
|
267
452
|
};
|
|
268
453
|
};
|
|
269
454
|
|
|
@@ -100,7 +100,8 @@ export const getBaseParams = (options = {}) => {
|
|
|
100
100
|
*/
|
|
101
101
|
export const getPaymentMethodParams = (paymentMethod, options = {}) => {
|
|
102
102
|
const {
|
|
103
|
-
cardType
|
|
103
|
+
cardType,
|
|
104
|
+
cardtype,
|
|
104
105
|
captureMode = "full",
|
|
105
106
|
cardpan,
|
|
106
107
|
cardexpiredate,
|
|
@@ -124,6 +125,9 @@ export const getPaymentMethodParams = (paymentMethod, options = {}) => {
|
|
|
124
125
|
country
|
|
125
126
|
} = options;
|
|
126
127
|
|
|
128
|
+
// Use cardtype if provided, otherwise fall back to cardType, otherwise default to "V"
|
|
129
|
+
const finalCardType = cardtype || cardType || "V";
|
|
130
|
+
|
|
127
131
|
// Helper to get shipping params for wallet payments
|
|
128
132
|
const getShippingParams = () => ({
|
|
129
133
|
shipping_firstname: shipping_firstname || firstname || "John",
|
|
@@ -138,7 +142,7 @@ export const getPaymentMethodParams = (paymentMethod, options = {}) => {
|
|
|
138
142
|
case "cc": // Credit Card (Visa, Mastercard, Amex)
|
|
139
143
|
return {
|
|
140
144
|
clearingtype: "cc",
|
|
141
|
-
cardtype:
|
|
145
|
+
cardtype: finalCardType, // V = Visa, M = Mastercard, A = Amex
|
|
142
146
|
cardpan: cardpan || "4111111111111111", // Test Visa card
|
|
143
147
|
cardexpiredate: cardexpiredate || "2512", // MMYY format
|
|
144
148
|
cardcvc2: cardcvc2 || "123" // 3-digit security code
|
package/package.json
CHANGED
|
@@ -41,27 +41,84 @@ const sendRequest = async (strapi, params) => {
|
|
|
41
41
|
|
|
42
42
|
const responseData = parseResponse(response.data, strapi.log);
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// Log full response for debugging
|
|
45
|
+
strapi.log.info("Payone API Response:", JSON.stringify(responseData, null, 2));
|
|
46
|
+
strapi.log.info("Response Status:", responseData.status || responseData.Status);
|
|
47
|
+
strapi.log.info("Response Error Code:", responseData.errorcode || responseData.ErrorCode || responseData.Error?.ErrorCode);
|
|
48
|
+
strapi.log.info("Response Error Message:", responseData.errormessage || responseData.ErrorMessage || responseData.Error?.ErrorMessage);
|
|
49
|
+
|
|
50
|
+
// Log all possible redirect URL fields
|
|
51
|
+
strapi.log.info("Redirect URL fields:", {
|
|
52
|
+
redirecturl: responseData.redirecturl,
|
|
53
|
+
RedirectUrl: responseData.RedirectUrl,
|
|
54
|
+
redirect_url: responseData.redirect_url,
|
|
55
|
+
redirectUrl: responseData.redirectUrl,
|
|
56
|
+
url: responseData.url,
|
|
57
|
+
Url: responseData.Url
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Extract error information from various possible fields
|
|
61
|
+
const errorCode =
|
|
62
|
+
responseData.errorcode ||
|
|
63
|
+
responseData.ErrorCode ||
|
|
64
|
+
responseData.Error?.ErrorCode ||
|
|
65
|
+
responseData.error_code ||
|
|
66
|
+
null;
|
|
67
|
+
|
|
68
|
+
// Check for 3DS redirect
|
|
69
|
+
const requires3DSErrorCodes = ["4219", 4219];
|
|
70
|
+
const is3DSRequiredError = requires3DSErrorCodes.includes(errorCode);
|
|
71
|
+
|
|
72
|
+
if (requires3DSRedirect(responseData) || is3DSRequiredError) {
|
|
45
73
|
const redirectUrl = get3DSRedirectUrl(responseData);
|
|
46
74
|
responseData.requires3DSRedirect = true;
|
|
47
75
|
responseData.redirectUrl = redirectUrl;
|
|
76
|
+
responseData.is3DSRequired = is3DSRequiredError;
|
|
77
|
+
|
|
78
|
+
// If 3DS required but no redirect URL, log for debugging
|
|
79
|
+
if (is3DSRequiredError && !redirectUrl) {
|
|
80
|
+
strapi.log.warn("3DS authentication required (Error 4219) but no redirect URL found. May need 3dscheck request.");
|
|
81
|
+
strapi.log.info("Full response data:", JSON.stringify(responseData, null, 2));
|
|
82
|
+
}
|
|
48
83
|
}
|
|
49
84
|
|
|
85
|
+
const errorMessage =
|
|
86
|
+
responseData.errormessage ||
|
|
87
|
+
responseData.ErrorMessage ||
|
|
88
|
+
responseData.Error?.ErrorMessage ||
|
|
89
|
+
responseData.error_message ||
|
|
90
|
+
null;
|
|
91
|
+
|
|
92
|
+
const customerMessage =
|
|
93
|
+
responseData.customermessage ||
|
|
94
|
+
responseData.CustomerMessage ||
|
|
95
|
+
responseData.Error?.CustomerMessage ||
|
|
96
|
+
responseData.customer_message ||
|
|
97
|
+
null;
|
|
98
|
+
|
|
99
|
+
const status = (responseData.status || responseData.Status || "unknown").toUpperCase();
|
|
100
|
+
|
|
50
101
|
// Log transaction
|
|
51
102
|
await logTransaction(strapi, {
|
|
52
103
|
txid: extractTxId(responseData) || params.txid || null,
|
|
53
104
|
reference: params.reference || null,
|
|
54
|
-
status:
|
|
105
|
+
status: status,
|
|
55
106
|
request_type: params.request,
|
|
56
107
|
amount: params.amount || null,
|
|
57
108
|
currency: params.currency || "EUR",
|
|
58
109
|
raw_request: requestParams,
|
|
59
110
|
raw_response: responseData,
|
|
60
|
-
error_code:
|
|
61
|
-
error_message:
|
|
62
|
-
customer_message:
|
|
111
|
+
error_code: errorCode,
|
|
112
|
+
error_message: errorMessage,
|
|
113
|
+
customer_message: customerMessage
|
|
63
114
|
});
|
|
64
115
|
|
|
116
|
+
// Add normalized error fields to response
|
|
117
|
+
responseData.errorCode = errorCode;
|
|
118
|
+
responseData.errorMessage = errorMessage;
|
|
119
|
+
responseData.customerMessage = customerMessage;
|
|
120
|
+
responseData.status = status;
|
|
121
|
+
|
|
65
122
|
return responseData;
|
|
66
123
|
} catch (error) {
|
|
67
124
|
strapi.log.error("Payone sendRequest error:", error);
|
|
@@ -42,6 +42,8 @@ const buildClientRequestParams = (settings, params, logger = null) => {
|
|
|
42
42
|
requestParams["3dsecure"] = "yes";
|
|
43
43
|
requestParams.ecommercemode = params.ecommercemode || "internet";
|
|
44
44
|
|
|
45
|
+
// Ensure redirect URLs are always provided for 3DS
|
|
46
|
+
// These are required for 3DS authentication flow
|
|
45
47
|
if (!requestParams.successurl) {
|
|
46
48
|
requestParams.successurl = params.successurl || "https://www.example.com/success";
|
|
47
49
|
}
|
|
@@ -51,6 +53,15 @@ const buildClientRequestParams = (settings, params, logger = null) => {
|
|
|
51
53
|
if (!requestParams.backurl) {
|
|
52
54
|
requestParams.backurl = params.backurl || "https://www.example.com/back";
|
|
53
55
|
}
|
|
56
|
+
|
|
57
|
+
// Log redirect URLs for debugging
|
|
58
|
+
if (logger) {
|
|
59
|
+
logger.info("3DS Redirect URLs:", {
|
|
60
|
+
successurl: requestParams.successurl,
|
|
61
|
+
errorurl: requestParams.errorurl,
|
|
62
|
+
backurl: requestParams.backurl
|
|
63
|
+
});
|
|
64
|
+
}
|
|
54
65
|
} else if (isCreditCard && !enable3DSecure) {
|
|
55
66
|
requestParams["3dsecure"] = "no";
|
|
56
67
|
}
|
|
@@ -54,9 +54,38 @@ const extractTxId = (data) => {
|
|
|
54
54
|
*/
|
|
55
55
|
const requires3DSRedirect = (data) => {
|
|
56
56
|
const status = (data.status || data.Status || "").toUpperCase();
|
|
57
|
-
const
|
|
57
|
+
const errorCode = data.errorcode || data.ErrorCode || data.Error?.ErrorCode;
|
|
58
|
+
|
|
59
|
+
// Check for redirect URL in various possible fields
|
|
60
|
+
const redirecturl =
|
|
61
|
+
data.redirecturl ||
|
|
62
|
+
data.RedirectUrl ||
|
|
63
|
+
data.redirect_url ||
|
|
64
|
+
data.redirectUrl ||
|
|
65
|
+
data.RedirectURL ||
|
|
66
|
+
data.redirectURL ||
|
|
67
|
+
data.url ||
|
|
68
|
+
data.Url ||
|
|
69
|
+
data.URL ||
|
|
70
|
+
null;
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
// 3DS required error codes (4219, etc.)
|
|
73
|
+
const requires3DSErrorCodes = ["4219", 4219];
|
|
74
|
+
const is3DSRequiredError = requires3DSErrorCodes.includes(errorCode);
|
|
75
|
+
|
|
76
|
+
return (status === "REDIRECT" && !!redirecturl) || is3DSRequiredError;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if response indicates an error
|
|
81
|
+
* @param {Object} data - Response data
|
|
82
|
+
* @returns {boolean} True if response indicates error
|
|
83
|
+
*/
|
|
84
|
+
const isErrorResponse = (data) => {
|
|
85
|
+
const status = (data.status || data.Status || "").toUpperCase();
|
|
86
|
+
const errorCode = data.errorcode || data.ErrorCode || data.Error?.ErrorCode;
|
|
87
|
+
|
|
88
|
+
return status === "ERROR" || status === "INVALID" || !!errorCode;
|
|
60
89
|
};
|
|
61
90
|
|
|
62
91
|
/**
|
|
@@ -65,9 +94,33 @@ const requires3DSRedirect = (data) => {
|
|
|
65
94
|
* @returns {string|null} Redirect URL
|
|
66
95
|
*/
|
|
67
96
|
const get3DSRedirectUrl = (data) => {
|
|
68
|
-
|
|
69
|
-
|
|
97
|
+
// Check all possible redirect URL fields
|
|
98
|
+
const redirecturl =
|
|
99
|
+
data.redirecturl ||
|
|
100
|
+
data.RedirectUrl ||
|
|
101
|
+
data.redirect_url ||
|
|
102
|
+
data.redirectUrl ||
|
|
103
|
+
data.RedirectURL ||
|
|
104
|
+
data.redirectURL ||
|
|
105
|
+
data.url ||
|
|
106
|
+
data.Url ||
|
|
107
|
+
data.URL ||
|
|
108
|
+
data.redirect ||
|
|
109
|
+
data.Redirect ||
|
|
110
|
+
null;
|
|
111
|
+
|
|
112
|
+
if (redirecturl) {
|
|
113
|
+
return redirecturl;
|
|
70
114
|
}
|
|
115
|
+
|
|
116
|
+
// If 3DS required but no redirect URL, might need 3dscheck
|
|
117
|
+
const errorCode = data.errorcode || data.ErrorCode || data.Error?.ErrorCode;
|
|
118
|
+
const requires3DSErrorCodes = ["4219", 4219];
|
|
119
|
+
if (requires3DSErrorCodes.includes(errorCode)) {
|
|
120
|
+
// Return null - will need to handle 3dscheck separately
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
71
124
|
return null;
|
|
72
125
|
};
|
|
73
126
|
|
|
@@ -75,6 +128,7 @@ module.exports = {
|
|
|
75
128
|
parseResponse,
|
|
76
129
|
extractTxId,
|
|
77
130
|
requires3DSRedirect,
|
|
78
|
-
get3DSRedirectUrl
|
|
131
|
+
get3DSRedirectUrl,
|
|
132
|
+
isErrorResponse
|
|
79
133
|
};
|
|
80
134
|
|