strapi-plugin-payone-provider 1.2.4 → 1.4.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 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 padding={4}>
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
 
@@ -6,7 +6,9 @@ const StatusBadge = ({ status }) => {
6
6
  APPROVED: "success",
7
7
  PENDING: "warning",
8
8
  ERROR: "danger",
9
- FAILED: "danger"
9
+ FAILED: "danger",
10
+ INVALID: "danger",
11
+ REDIRECT: "secondary"
10
12
  };
11
13
 
12
14
  return (
@@ -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="Enter reference"
61
- hint="Reference for this transaction"
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={!paymentAmount.trim() || !authReference.trim()}
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={{ flex: 1, textAlign: "right", fontWeight: '400' }}
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="Enter reference"
61
- hint="Reference for this transaction"
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={!paymentAmount.trim() || !preauthReference.trim()}
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
- const [preauthReference, setPreauthReference] = useState("");
35
- const [authReference, setAuthReference] = useState("");
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: "EUR",
76
- reference: preauthReference || `PREAUTH-${Date.now()}`,
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
- (responseData.status === "REDIRECT" && redirectUrl) ||
107
- (responseData.Status === "REDIRECT" && redirectUrl);
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
- handlePaymentSuccess("Preauthorization completed successfully");
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: "EUR",
132
- reference: authReference || `AUTH-${Date.now()}`,
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
- const redirectUrl = responseData.redirectUrl || responseData.redirecturl || responseData.RedirectUrl;
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
- (responseData.status === "REDIRECT" && redirectUrl) ||
162
- (responseData.Status === "REDIRECT" && redirectUrl);
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
- handlePaymentSuccess("Authorization completed successfully");
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 = "V",
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: cardType, // V = Visa, M = Mastercard, A = Amex
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-payone-provider",
3
- "version": "1.2.4",
3
+ "version": "1.4.0",
4
4
  "description": "Strapi plugin for Payone payment gateway integration",
5
5
  "license": "MIT",
6
6
  "maintainers": [
@@ -43,12 +43,25 @@ module.exports = ({ strapi }) => ({
43
43
  }
44
44
  },
45
45
 
46
+ async getPublicSettings(ctx) {
47
+ try {
48
+ const settings = await getPayoneService(strapi).getSettings();
49
+ ctx.body = {
50
+ data: {
51
+ mid: settings?.mid || null,
52
+ mode: settings?.mode || null
53
+ }
54
+ };
55
+ } catch (error) {
56
+ handleError(ctx, error);
57
+ }
58
+ },
59
+
46
60
  async updateSettings(ctx) {
47
61
  try {
48
62
  const { body } = ctx.request;
49
63
  const currentSettings = await getPayoneService(strapi).getSettings();
50
64
 
51
- // Preserve existing key if hidden or not provided
52
65
  if (body.key === "***HIDDEN***" || !body.key) {
53
66
  body.key = currentSettings?.key;
54
67
  }
@@ -82,6 +82,15 @@ module.exports = {
82
82
  "content-api": {
83
83
  type: "content-api",
84
84
  routes: [
85
+ {
86
+ method: "GET",
87
+ path: "/settings",
88
+ handler: "payone.getPublicSettings",
89
+ config: {
90
+ policies: ["plugin::strapi-plugin-payone-provider.is-auth"],
91
+ auth: false
92
+ }
93
+ },
85
94
  {
86
95
  method: "POST",
87
96
  path: "/preauthorization",
@@ -41,27 +41,84 @@ const sendRequest = async (strapi, params) => {
41
41
 
42
42
  const responseData = parseResponse(response.data, strapi.log);
43
43
 
44
- if (requires3DSRedirect(responseData)) {
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: responseData.status || responseData.Status || "unknown",
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: responseData.Error?.ErrorCode || null,
61
- error_message: responseData.Error?.ErrorMessage || null,
62
- customer_message: responseData.Error?.CustomerMessage || null
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 redirecturl = data.redirecturl || data.RedirectUrl || data.redirect_url;
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
- return status === "REDIRECT" && !!redirecturl;
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
- if (requires3DSRedirect(data)) {
69
- return data.redirecturl || data.RedirectUrl || data.redirect_url || null;
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