@akinon/pz-saved-card 2.0.0-beta.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -17
- package/package.json +10 -10
- package/src/components/agreement-and-submit.tsx +20 -5
- package/src/components/card-form-section.tsx +127 -0
- package/src/components/card-selection-section.tsx +2 -2
- package/src/components/installment-section.tsx +2 -4
- package/src/components/installments.tsx +4 -6
- package/src/index.tsx +7 -1
- package/src/redux/api.ts +34 -7
- package/src/redux/middleware.ts +12 -6
- package/src/redux/reducer.ts +21 -2
- package/src/types/index.ts +51 -4
- package/src/views/iyzico-saved-card-option.tsx +429 -0
- package/src/views/saved-card-option.tsx +23 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,45 +1,153 @@
|
|
|
1
1
|
# @akinon/pz-saved-card
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
3
|
+
## 2.0.1
|
|
4
|
+
|
|
5
|
+
## 2.0.0
|
|
6
|
+
|
|
7
|
+
## 2.0.0-beta.27
|
|
8
|
+
|
|
9
|
+
## 2.0.0-beta.26
|
|
10
|
+
|
|
11
|
+
## 2.0.0-beta.25
|
|
12
|
+
|
|
13
|
+
## 2.0.0-beta.24
|
|
14
|
+
|
|
15
|
+
## 2.0.0-beta.23
|
|
16
|
+
|
|
17
|
+
## 2.0.0-beta.22
|
|
18
|
+
|
|
19
|
+
## 2.0.0-beta.21
|
|
20
|
+
|
|
21
|
+
## 2.0.0-beta.20
|
|
22
|
+
|
|
23
|
+
## 1.126.0
|
|
24
|
+
|
|
25
|
+
## 1.125.2
|
|
26
|
+
|
|
27
|
+
## 1.125.1
|
|
28
|
+
|
|
29
|
+
## 1.125.0
|
|
30
|
+
|
|
31
|
+
## 1.124.0
|
|
32
|
+
|
|
33
|
+
## 1.123.0
|
|
34
|
+
|
|
35
|
+
## 1.122.0
|
|
36
|
+
|
|
37
|
+
## 1.121.0
|
|
38
|
+
|
|
39
|
+
## 1.120.0
|
|
40
|
+
|
|
41
|
+
### Minor Changes
|
|
42
|
+
|
|
43
|
+
- 6ad72e8d: ZERO-4032: Add loading state management for payment submissions across multiple components and add safe guarding
|
|
44
|
+
|
|
45
|
+
## 1.119.0
|
|
46
|
+
|
|
47
|
+
## 1.118.0
|
|
48
|
+
|
|
49
|
+
## 1.117.0
|
|
50
|
+
|
|
51
|
+
## 1.116.0
|
|
52
|
+
|
|
53
|
+
## 1.115.0
|
|
54
|
+
|
|
55
|
+
## 1.114.0
|
|
56
|
+
|
|
57
|
+
## 1.113.0
|
|
58
|
+
|
|
59
|
+
## 1.112.0
|
|
60
|
+
|
|
61
|
+
## 1.111.0
|
|
62
|
+
|
|
63
|
+
## 1.110.0
|
|
64
|
+
|
|
65
|
+
## 1.109.0
|
|
66
|
+
|
|
67
|
+
## 1.108.0
|
|
68
|
+
|
|
69
|
+
## 1.107.0
|
|
4
70
|
|
|
5
71
|
### Minor Changes
|
|
6
72
|
|
|
7
|
-
-
|
|
73
|
+
- 4ca44c78: ZERO-3634: add register_consumer_card
|
|
74
|
+
- 6bfbdc27: ZERO-3653: optimize saved card option
|
|
75
|
+
- 5b500797: ZERO-3634: iyzico saved card
|
|
76
|
+
- 9442cf01: ZERO-3653: make the register_consumer_card option optional
|
|
8
77
|
|
|
9
|
-
##
|
|
78
|
+
## 1.106.0
|
|
10
79
|
|
|
11
|
-
##
|
|
80
|
+
## 1.105.0
|
|
12
81
|
|
|
13
|
-
##
|
|
82
|
+
## 1.104.0
|
|
83
|
+
|
|
84
|
+
## 1.103.0
|
|
85
|
+
|
|
86
|
+
## 1.102.0
|
|
87
|
+
|
|
88
|
+
## 1.101.0
|
|
89
|
+
|
|
90
|
+
## 1.100.0
|
|
91
|
+
|
|
92
|
+
## 1.99.0
|
|
14
93
|
|
|
15
94
|
### Minor Changes
|
|
16
95
|
|
|
17
|
-
-
|
|
96
|
+
- d58538b: ZERO-3638: Enhance RC pipeline: add fetch, merge, and pre-release setup with conditional commit
|
|
97
|
+
|
|
98
|
+
## 1.98.0
|
|
99
|
+
|
|
100
|
+
## 1.97.0
|
|
18
101
|
|
|
19
|
-
##
|
|
102
|
+
## 1.96.0
|
|
20
103
|
|
|
21
|
-
##
|
|
104
|
+
## 1.95.0
|
|
22
105
|
|
|
23
|
-
##
|
|
106
|
+
## 1.94.0
|
|
24
107
|
|
|
25
|
-
##
|
|
108
|
+
## 1.93.0
|
|
109
|
+
|
|
110
|
+
## 1.92.0
|
|
111
|
+
|
|
112
|
+
## 1.91.0
|
|
113
|
+
|
|
114
|
+
## 1.90.0
|
|
115
|
+
|
|
116
|
+
## 1.89.0
|
|
26
117
|
|
|
27
118
|
### Minor Changes
|
|
28
119
|
|
|
29
|
-
-
|
|
30
|
-
|
|
120
|
+
- e2026ec: ZERO-3353: add missing test ids for e2e test
|
|
121
|
+
|
|
122
|
+
## 1.88.0
|
|
123
|
+
|
|
124
|
+
## 1.87.0
|
|
125
|
+
|
|
126
|
+
## 1.86.0
|
|
31
127
|
|
|
32
|
-
##
|
|
128
|
+
## 1.85.0
|
|
129
|
+
|
|
130
|
+
## 1.84.0
|
|
33
131
|
|
|
34
132
|
### Minor Changes
|
|
35
133
|
|
|
36
|
-
- ZERO-
|
|
134
|
+
- 624a4eb: ZERO-3276: Update installation instructions across multiple README files to standardize format and improve clarity
|
|
135
|
+
|
|
136
|
+
## 1.83.0
|
|
137
|
+
|
|
138
|
+
## 1.82.0
|
|
139
|
+
|
|
140
|
+
## 1.81.0
|
|
141
|
+
|
|
142
|
+
## 1.80.0
|
|
143
|
+
|
|
144
|
+
## 1.79.0
|
|
37
145
|
|
|
38
|
-
##
|
|
146
|
+
## 1.78.0
|
|
39
147
|
|
|
40
|
-
|
|
148
|
+
## 1.77.0
|
|
41
149
|
|
|
42
|
-
|
|
150
|
+
## 1.76.0
|
|
43
151
|
|
|
44
152
|
## 1.75.0
|
|
45
153
|
|
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-saved-card",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"peerDependencies": {
|
|
7
|
-
"react": "^19.0.0",
|
|
8
|
-
"react-dom": "^19.0.0"
|
|
7
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
8
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"react-redux": "8.1.3"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@types/node": "^
|
|
15
|
-
"@types/react": "^
|
|
16
|
-
"@types/react-dom": "^
|
|
17
|
-
"prettier": "^3.
|
|
18
|
-
"react": "
|
|
19
|
-
"react-dom": "
|
|
20
|
-
"typescript": "^5.
|
|
14
|
+
"@types/node": "^18.7.8",
|
|
15
|
+
"@types/react": "^18.0.17",
|
|
16
|
+
"@types/react-dom": "^18.0.6",
|
|
17
|
+
"prettier": "^3.0.3",
|
|
18
|
+
"react": "19.2.5",
|
|
19
|
+
"react-dom": "19.2.5",
|
|
20
|
+
"typescript": "^5.2.2"
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -1,22 +1,37 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { cloneElement } from 'react';
|
|
2
2
|
import { Button, Icon } from '@akinon/next/components';
|
|
3
|
+
import { AgreementAndSubmitProps } from '../types';
|
|
3
4
|
|
|
4
5
|
export const AgreementAndSubmit = ({
|
|
5
6
|
agreementCheckbox,
|
|
6
7
|
control,
|
|
7
8
|
errors,
|
|
8
|
-
buttonText
|
|
9
|
-
|
|
9
|
+
buttonText,
|
|
10
|
+
formError,
|
|
11
|
+
isSubmitting
|
|
12
|
+
}: AgreementAndSubmitProps) => (
|
|
10
13
|
<div className="flex flex-col text-xs pb-4 px-4 sm:px-6">
|
|
11
14
|
{agreementCheckbox &&
|
|
12
|
-
cloneElement(agreementCheckbox, {
|
|
13
|
-
control,
|
|
15
|
+
cloneElement(agreementCheckbox as any, {
|
|
16
|
+
control: control as any,
|
|
14
17
|
error: errors.agreement,
|
|
15
18
|
fieldId: 'agreement'
|
|
16
19
|
})}
|
|
20
|
+
{formError?.non_field_errors && (
|
|
21
|
+
<div className="w-full text-xs text-start px-1 mt-3 text-error">
|
|
22
|
+
{formError.non_field_errors}
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
{formError?.status && (
|
|
26
|
+
<div className="w-full text-xs text-start px-1 mt-3 text-error">
|
|
27
|
+
{formError.status}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
17
30
|
<Button
|
|
18
31
|
type="submit"
|
|
19
32
|
className="group uppercase mt-4 inline-flex items-center justify-center"
|
|
33
|
+
disabled={isSubmitting}
|
|
34
|
+
data-testid="saved-card-submit-button"
|
|
20
35
|
>
|
|
21
36
|
<span>{buttonText}</span>
|
|
22
37
|
<Icon name="chevron-end" size={12} className="ml-2 h-3" />
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Input, Select, Icon, Image } from '@akinon/next/components';
|
|
3
|
+
import { Control, UseFormRegister, FieldErrors } from 'react-hook-form';
|
|
4
|
+
|
|
5
|
+
export interface CardFormSectionProps {
|
|
6
|
+
register: UseFormRegister<any>;
|
|
7
|
+
control: Control<any>;
|
|
8
|
+
errors: FieldErrors;
|
|
9
|
+
months: Array<{ label: string; value: string }>;
|
|
10
|
+
years: Array<{ label: string; value: string }>;
|
|
11
|
+
translations?: {
|
|
12
|
+
cardHolder?: string;
|
|
13
|
+
cardNumber?: string;
|
|
14
|
+
expiryDate?: string;
|
|
15
|
+
cvv?: string;
|
|
16
|
+
cvvTooltip?: string;
|
|
17
|
+
saveCardForFuture?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const CardFormSection = ({
|
|
22
|
+
register,
|
|
23
|
+
control,
|
|
24
|
+
errors,
|
|
25
|
+
months,
|
|
26
|
+
years,
|
|
27
|
+
translations = {},
|
|
28
|
+
}: CardFormSectionProps) => {
|
|
29
|
+
return (
|
|
30
|
+
<div className="w-full bg-white">
|
|
31
|
+
<div className="px-4 my-2 w-full flex justify-between flex-wrap">
|
|
32
|
+
<div className="my-2 w-full sm:px-4">
|
|
33
|
+
<Input
|
|
34
|
+
label={translations.cardHolder}
|
|
35
|
+
{...register('card_holder')}
|
|
36
|
+
error={errors.card_holder}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div className="my-2 w-full flex flex-col sm:px-4">
|
|
41
|
+
<div className="text-xs text-gray-800 mb-2 w-full flex justify-between items-center">
|
|
42
|
+
<span>{translations.cardNumber}</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<Input
|
|
46
|
+
format="#### #### #### ####"
|
|
47
|
+
mask="_"
|
|
48
|
+
allowEmptyFormatting={true}
|
|
49
|
+
control={control}
|
|
50
|
+
{...register('card_number')}
|
|
51
|
+
error={errors.card_number}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="w-full my-2 sm:flex">
|
|
56
|
+
<div className="sm:w-2/3 sm:px-4">
|
|
57
|
+
<label
|
|
58
|
+
className="flex w-full text-xs text-start text-black-400 mb-1.5"
|
|
59
|
+
htmlFor="card_month"
|
|
60
|
+
>
|
|
61
|
+
{translations.expiryDate}
|
|
62
|
+
</label>
|
|
63
|
+
|
|
64
|
+
<div className="flex w-full h-10 space-x-2.5">
|
|
65
|
+
<div className="w-2/4">
|
|
66
|
+
<Select
|
|
67
|
+
className="w-full text-xs border-gray-400 sm:text-sm"
|
|
68
|
+
options={months}
|
|
69
|
+
{...register('card_month')}
|
|
70
|
+
error={errors.card_month}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="w-2/4">
|
|
75
|
+
<Select
|
|
76
|
+
className="w-full text-xs border-gray-400 sm:text-sm"
|
|
77
|
+
options={years}
|
|
78
|
+
{...register('card_year')}
|
|
79
|
+
error={errors.card_year}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="my-2 sm:w-1/3 sm:px-4 sm:my-0">
|
|
86
|
+
<label
|
|
87
|
+
className="flex w-full text-xs text-start text-black-400 mb-1.5"
|
|
88
|
+
htmlFor="card_cvv"
|
|
89
|
+
>
|
|
90
|
+
{translations.cvv}
|
|
91
|
+
</label>
|
|
92
|
+
<Input
|
|
93
|
+
format="###"
|
|
94
|
+
mask="_"
|
|
95
|
+
control={control}
|
|
96
|
+
allowEmptyFormatting={true}
|
|
97
|
+
{...register('card_cvv')}
|
|
98
|
+
error={errors.card_cvv}
|
|
99
|
+
/>
|
|
100
|
+
<div className="group relative flex items-center justify-start text-gray-600 cursor-pointer mt-2 transition-all hover:text-secondary">
|
|
101
|
+
<span className="text-xs underline">
|
|
102
|
+
{translations.cvv}
|
|
103
|
+
</span>
|
|
104
|
+
<Icon name="cvc" size={16} className="leading-none ml-2" />
|
|
105
|
+
<div className="hidden group-hover:block absolute right-0 bottom-5 w-[11rem] lg:w-[21rem] lg:left-auto lg:right-auto border-2">
|
|
106
|
+
<Image src="/cvv.jpg" alt="Cvv" width={385} height={264} />
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className="my-2 w-full flex flex-col sm:px-4">
|
|
113
|
+
<label className="flex items-center space-x-2">
|
|
114
|
+
<input
|
|
115
|
+
type="checkbox"
|
|
116
|
+
{...register('register_consumer_card')}
|
|
117
|
+
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
|
118
|
+
/>
|
|
119
|
+
<span className="text-sm text-gray-700">
|
|
120
|
+
{translations.saveCardForFuture}
|
|
121
|
+
</span>
|
|
122
|
+
</label>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { setDeletionModalId, setDeletionModalVisible } from '../redux/reducer';
|
|
2
|
-
import React from 'react';
|
|
3
2
|
import { ErrorText } from './error-text';
|
|
4
3
|
import { CardLabel } from './card-label';
|
|
5
4
|
import { DeleteIcon } from './delete-icon';
|
|
@@ -16,7 +15,7 @@ export const CardSelectionSection = ({
|
|
|
16
15
|
<div className="border-solid border-gray-400 px-4 py-2">
|
|
17
16
|
<span className="text-black-800 text-lg font-medium">{title}</span>
|
|
18
17
|
<ul className="mt-4 text-xs w-full">
|
|
19
|
-
{cards?.map((card) => (
|
|
18
|
+
{cards?.map((card, index) => (
|
|
20
19
|
<li
|
|
21
20
|
key={card.token}
|
|
22
21
|
className="p-4 mb-2 border-2 border-gray-200 flex justify-between items-center cursor-pointer"
|
|
@@ -24,6 +23,7 @@ export const CardSelectionSection = ({
|
|
|
24
23
|
e.preventDefault();
|
|
25
24
|
onSelect(card);
|
|
26
25
|
}}
|
|
26
|
+
data-testid={`saved-card-item-${index}`}
|
|
27
27
|
>
|
|
28
28
|
<input
|
|
29
29
|
name="card"
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import SavedCardInstallments from './installments';
|
|
2
|
-
import
|
|
2
|
+
import { InstallmentSectionProps } from '../types';
|
|
3
3
|
|
|
4
4
|
export const InstallmentSection = ({
|
|
5
5
|
title,
|
|
6
|
-
selectedCard,
|
|
7
6
|
installmentOptions,
|
|
8
7
|
translations,
|
|
9
8
|
errors
|
|
10
|
-
}) => (
|
|
9
|
+
}: InstallmentSectionProps) => (
|
|
11
10
|
<div className="border-solid border-gray-400 bg-white">
|
|
12
11
|
<div className="px-4 py-2">
|
|
13
12
|
<span className="text-black-800 text-lg font-medium">{title}</span>
|
|
14
13
|
</div>
|
|
15
14
|
<SavedCardInstallments
|
|
16
|
-
selectedCard={selectedCard}
|
|
17
15
|
installmentOptions={installmentOptions}
|
|
18
16
|
translations={translations}
|
|
19
17
|
error={errors}
|
|
@@ -12,14 +12,12 @@ const defaultTranslations = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
type SavedCardInstallmentsProps = {
|
|
15
|
-
selectedCard: SavedCard;
|
|
16
15
|
installmentOptions: any[];
|
|
17
16
|
translations?: InstallmentTexts;
|
|
18
17
|
error: any;
|
|
19
18
|
};
|
|
20
19
|
|
|
21
20
|
const SavedCardInstallments = ({
|
|
22
|
-
selectedCard,
|
|
23
21
|
installmentOptions = [],
|
|
24
22
|
translations,
|
|
25
23
|
error
|
|
@@ -30,7 +28,6 @@ const SavedCardInstallments = ({
|
|
|
30
28
|
|
|
31
29
|
useEffect(() => {
|
|
32
30
|
if (
|
|
33
|
-
selectedCard &&
|
|
34
31
|
installmentOptions.length > 0 &&
|
|
35
32
|
installmentOptions[0]?.pk !== installmentOption
|
|
36
33
|
) {
|
|
@@ -40,7 +37,7 @@ const SavedCardInstallments = ({
|
|
|
40
37
|
});
|
|
41
38
|
setInstallmentOption(firstOptionPk);
|
|
42
39
|
}
|
|
43
|
-
}, [installmentOptions, installmentOption, setInstallment
|
|
40
|
+
}, [installmentOptions, installmentOption, setInstallment]);
|
|
44
41
|
|
|
45
42
|
if (installmentOptions.length === 0) {
|
|
46
43
|
return (
|
|
@@ -68,7 +65,7 @@ const SavedCardInstallments = ({
|
|
|
68
65
|
</tr>
|
|
69
66
|
</thead>
|
|
70
67
|
<tbody>
|
|
71
|
-
{installmentOptions.map((option) => (
|
|
68
|
+
{installmentOptions.map((option, index) => (
|
|
72
69
|
<tr
|
|
73
70
|
key={`installment-${option.pk}`}
|
|
74
71
|
className="border-t border-solid border-gray-400"
|
|
@@ -84,6 +81,7 @@ const SavedCardInstallments = ({
|
|
|
84
81
|
});
|
|
85
82
|
setInstallmentOption(option.pk);
|
|
86
83
|
}}
|
|
84
|
+
data-testid={`saved-card-installment-${index}`}
|
|
87
85
|
>
|
|
88
86
|
<span className="w-full flex items-center justify-start pl-2">
|
|
89
87
|
<span className="text-xs text-black-800 transition-all">
|
|
@@ -104,7 +102,7 @@ const SavedCardInstallments = ({
|
|
|
104
102
|
</table>
|
|
105
103
|
</div>
|
|
106
104
|
{error && (
|
|
107
|
-
<div className="px-6 mt-4 text-sm text-error">{error.message}</div>
|
|
105
|
+
<div className="px-6 mt-4 text-sm text-error">{String(error.message)}</div>
|
|
108
106
|
)}
|
|
109
107
|
</div>
|
|
110
108
|
);
|
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import savedCardReducer from './redux/reducer';
|
|
2
2
|
import savedCardMiddleware from './redux/middleware';
|
|
3
3
|
import SavedCardOption from './views/saved-card-option';
|
|
4
|
+
import IyzicoSavedCardOption from './views/iyzico-saved-card-option';
|
|
4
5
|
|
|
5
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
savedCardReducer,
|
|
8
|
+
savedCardMiddleware,
|
|
9
|
+
SavedCardOption,
|
|
10
|
+
IyzicoSavedCardOption
|
|
11
|
+
};
|
package/src/redux/api.ts
CHANGED
|
@@ -21,20 +21,49 @@ interface GetResponse<T> {
|
|
|
21
21
|
results: T[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
type
|
|
24
|
+
type DefaultSetSavedCardRequest = {
|
|
25
25
|
card: SavedCard;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
type IyzicoSetSavedCardRequest = {
|
|
29
|
+
card: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type SetSavedCardRequest =
|
|
33
|
+
| DefaultSetSavedCardRequest
|
|
34
|
+
| IyzicoSetSavedCardRequest;
|
|
35
|
+
|
|
28
36
|
type DeleteSavedCardRequest = number;
|
|
29
37
|
|
|
30
38
|
type SetInstallmentRequest = {
|
|
31
39
|
installment: string;
|
|
32
40
|
};
|
|
33
41
|
|
|
34
|
-
type
|
|
42
|
+
type DefaultCompleteSavedCardRequest = {
|
|
35
43
|
agreement: boolean;
|
|
36
44
|
};
|
|
37
45
|
|
|
46
|
+
type IyzicoCompleteSavedCardRequest =
|
|
47
|
+
| {
|
|
48
|
+
agreement: boolean;
|
|
49
|
+
register_consumer_card: boolean;
|
|
50
|
+
consumer_token: string;
|
|
51
|
+
card_token: string;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
agreement: boolean;
|
|
55
|
+
card_holder: string;
|
|
56
|
+
card_number: string;
|
|
57
|
+
card_cvv: string;
|
|
58
|
+
card_month: string;
|
|
59
|
+
card_year: string;
|
|
60
|
+
register_consumer_card: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type CompleteSavedCardRequest =
|
|
64
|
+
| DefaultCompleteSavedCardRequest
|
|
65
|
+
| IyzicoCompleteSavedCardRequest;
|
|
66
|
+
|
|
38
67
|
export const savedCardApi = api.injectEndpoints({
|
|
39
68
|
endpoints: (build) => ({
|
|
40
69
|
getSavedCards: build.query<GetResponse<SavedCard>, void>({
|
|
@@ -71,7 +100,7 @@ export const savedCardApi = api.injectEndpoints({
|
|
|
71
100
|
),
|
|
72
101
|
method: 'POST',
|
|
73
102
|
body: {
|
|
74
|
-
card: card.token
|
|
103
|
+
card: typeof card === 'string' ? card : card.token
|
|
75
104
|
}
|
|
76
105
|
}),
|
|
77
106
|
async onQueryStarted({ card }, { dispatch, queryFulfilled }) {
|
|
@@ -106,7 +135,7 @@ export const savedCardApi = api.injectEndpoints({
|
|
|
106
135
|
CheckoutResponse,
|
|
107
136
|
CompleteSavedCardRequest
|
|
108
137
|
>({
|
|
109
|
-
query: (
|
|
138
|
+
query: (body) => ({
|
|
110
139
|
url: buildClientRequestUrl(
|
|
111
140
|
`/orders/checkout?page=CompleteSavedCardRequest`,
|
|
112
141
|
{
|
|
@@ -114,9 +143,7 @@ export const savedCardApi = api.injectEndpoints({
|
|
|
114
143
|
}
|
|
115
144
|
),
|
|
116
145
|
method: 'POST',
|
|
117
|
-
body
|
|
118
|
-
agreement
|
|
119
|
-
}
|
|
146
|
+
body
|
|
120
147
|
}),
|
|
121
148
|
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
|
122
149
|
dispatch(setPaymentStepBusy(true));
|
package/src/redux/middleware.ts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { Middleware } from '@reduxjs/toolkit';
|
|
2
|
-
import {
|
|
2
|
+
import { CheckoutResult } from '@akinon/next/types';
|
|
3
|
+
import { setCards, setUcs } from './reducer';
|
|
3
4
|
|
|
4
5
|
const savedCardMiddleware: Middleware = ({ dispatch }) => {
|
|
5
6
|
return (next) => (action) => {
|
|
6
|
-
const result = next(action);
|
|
7
|
+
const result = next(action) as CheckoutResult;
|
|
7
8
|
|
|
8
9
|
const savedCardContext = result.payload?.context_list?.find(
|
|
9
|
-
(context) =>
|
|
10
|
-
context.page_slug === 'savedcardselectionpage' &&
|
|
11
|
-
context.page_context.cards
|
|
10
|
+
(context) => context.page_slug === 'savedcardselectionpage'
|
|
12
11
|
);
|
|
13
12
|
|
|
14
|
-
if (savedCardContext) {
|
|
13
|
+
if (savedCardContext && savedCardContext.page_context?.cards) {
|
|
15
14
|
const cards = JSON.parse(
|
|
16
15
|
JSON.stringify(savedCardContext.page_context.cards)
|
|
17
16
|
);
|
|
18
17
|
|
|
19
18
|
dispatch(setCards(cards));
|
|
19
|
+
} else if (savedCardContext && savedCardContext.page_context?.ucs) {
|
|
20
|
+
const ucs = JSON.parse(JSON.stringify(savedCardContext.page_context.ucs));
|
|
21
|
+
|
|
22
|
+
const match = ucs.script.match(/<script\b[^>]*>([\s\S]*?)<\/script>/);
|
|
23
|
+
ucs.script = match ? match[1] : ucs.script;
|
|
24
|
+
|
|
25
|
+
dispatch(setUcs(ucs));
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
return result;
|
package/src/redux/reducer.ts
CHANGED
|
@@ -7,12 +7,20 @@ export type SavedCard = {
|
|
|
7
7
|
token: string;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export type IyzicoUcs = {
|
|
11
|
+
maskedGsmNumber?: string;
|
|
12
|
+
script?: string;
|
|
13
|
+
scriptType?: string;
|
|
14
|
+
ucsToken?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
10
17
|
export interface SavedCardState {
|
|
11
18
|
cards?: Array<SavedCard>;
|
|
12
19
|
deletion: {
|
|
13
20
|
id: number;
|
|
14
21
|
isModalVisible: boolean;
|
|
15
22
|
};
|
|
23
|
+
ucs?: IyzicoUcs;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
const initialState: SavedCardState = {
|
|
@@ -35,11 +43,22 @@ const savedCardSlice = createSlice({
|
|
|
35
43
|
},
|
|
36
44
|
setDeletionModalVisible(state, { payload }) {
|
|
37
45
|
state.deletion.isModalVisible = payload;
|
|
46
|
+
},
|
|
47
|
+
setUcs(state, action: { payload: IyzicoUcs }) {
|
|
48
|
+
state.ucs = action.payload;
|
|
49
|
+
},
|
|
50
|
+
resetUcs() {
|
|
51
|
+
return initialState;
|
|
38
52
|
}
|
|
39
53
|
}
|
|
40
54
|
});
|
|
41
55
|
|
|
42
|
-
export const {
|
|
43
|
-
|
|
56
|
+
export const {
|
|
57
|
+
setCards,
|
|
58
|
+
setDeletionModalId,
|
|
59
|
+
setDeletionModalVisible,
|
|
60
|
+
setUcs,
|
|
61
|
+
resetUcs
|
|
62
|
+
} = savedCardSlice.actions;
|
|
44
63
|
|
|
45
64
|
export default savedCardSlice.reducer;
|
package/src/types/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
|
+
import { Control, FieldErrors } from 'react-hook-form';
|
|
2
3
|
|
|
3
4
|
export type InstallmentTexts = {
|
|
4
5
|
title?: string;
|
|
@@ -15,6 +16,9 @@ export type DeletePopupTexts = {
|
|
|
15
16
|
|
|
16
17
|
export type ErrorTexts = {
|
|
17
18
|
required?: string;
|
|
19
|
+
cardNumberLength?: string;
|
|
20
|
+
cvvLength?: string;
|
|
21
|
+
cardHolderMatches?: string;
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
export type SavedCardOptionTexts = {
|
|
@@ -25,6 +29,29 @@ export type SavedCardOptionTexts = {
|
|
|
25
29
|
errors?: ErrorTexts;
|
|
26
30
|
};
|
|
27
31
|
|
|
32
|
+
export type IyzicoSavedCardOptionTexts = {
|
|
33
|
+
title?: string;
|
|
34
|
+
button?: string;
|
|
35
|
+
installment?: InstallmentTexts;
|
|
36
|
+
errors?: ErrorTexts & {
|
|
37
|
+
saveCardRequired?: string;
|
|
38
|
+
};
|
|
39
|
+
label?: {
|
|
40
|
+
cardHolder?: string;
|
|
41
|
+
cardNumber?: string;
|
|
42
|
+
expiryDate?: string;
|
|
43
|
+
cvv?: string;
|
|
44
|
+
saveCardForFuture?: string;
|
|
45
|
+
};
|
|
46
|
+
placeholder?: {
|
|
47
|
+
cardHolder?: string;
|
|
48
|
+
cardMonth?: string;
|
|
49
|
+
cardYear?: string;
|
|
50
|
+
};
|
|
51
|
+
savedCardsTitle?: string;
|
|
52
|
+
paymentErrorMessage?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
28
55
|
export type CardSelectionSectionProps = {
|
|
29
56
|
title: string;
|
|
30
57
|
cards: any[];
|
|
@@ -37,15 +64,35 @@ export type CardSelectionSectionProps = {
|
|
|
37
64
|
|
|
38
65
|
export type InstallmentSectionProps = {
|
|
39
66
|
title: string;
|
|
40
|
-
selectedCard: any;
|
|
41
67
|
installmentOptions: any;
|
|
42
68
|
translations: InstallmentTexts;
|
|
43
69
|
errors: any;
|
|
70
|
+
setFormValue?: any;
|
|
44
71
|
};
|
|
45
72
|
|
|
46
|
-
export
|
|
47
|
-
agreementCheckbox
|
|
73
|
+
export interface AgreementAndSubmitProps {
|
|
74
|
+
agreementCheckbox?: ReactElement;
|
|
75
|
+
control: Control<any>;
|
|
76
|
+
errors: FieldErrors;
|
|
77
|
+
buttonText: string;
|
|
78
|
+
formError?: {
|
|
79
|
+
non_field_errors?: string;
|
|
80
|
+
status?: string;
|
|
81
|
+
[key: string]: any;
|
|
82
|
+
};
|
|
83
|
+
isSubmitting?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type CardFormSectionProps = {
|
|
87
|
+
register: any;
|
|
48
88
|
control: any;
|
|
49
89
|
errors: any;
|
|
50
|
-
|
|
90
|
+
months: Array<{ label: string; value: string }>;
|
|
91
|
+
years: Array<{ label: string; value: string }>;
|
|
92
|
+
translations?: IyzicoSavedCardOptionTexts['label'];
|
|
93
|
+
setFormValue?: any;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type IyzicoSavedCardsProps = {
|
|
97
|
+
title?: string;
|
|
51
98
|
};
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as yup from 'yup';
|
|
4
|
+
import { ObjectSchema } from 'yup';
|
|
5
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
6
|
+
import { useForm } from 'react-hook-form';
|
|
7
|
+
import {
|
|
8
|
+
FormHTMLAttributes,
|
|
9
|
+
HTMLAttributes,
|
|
10
|
+
ReactElement,
|
|
11
|
+
useEffect,
|
|
12
|
+
useMemo,
|
|
13
|
+
useState
|
|
14
|
+
} from 'react';
|
|
15
|
+
import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
16
|
+
import {
|
|
17
|
+
useCompleteSavedCardMutation,
|
|
18
|
+
useSetSavedCardMutation
|
|
19
|
+
} from '../redux/api';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
AgreementAndSubmitProps,
|
|
23
|
+
CardFormSectionProps,
|
|
24
|
+
ErrorTexts,
|
|
25
|
+
InstallmentSectionProps,
|
|
26
|
+
IyzicoSavedCardOptionTexts,
|
|
27
|
+
IyzicoSavedCardsProps
|
|
28
|
+
} from '../types';
|
|
29
|
+
import { InstallmentSection } from '../components/installment-section';
|
|
30
|
+
import { AgreementAndSubmit } from '../components/agreement-and-submit';
|
|
31
|
+
import { CardFormSection } from '../components/card-form-section';
|
|
32
|
+
import Script from 'next/script';
|
|
33
|
+
|
|
34
|
+
type IyzicoSavedCardOptionProps = {
|
|
35
|
+
texts?: IyzicoSavedCardOptionTexts;
|
|
36
|
+
agreementCheckbox?: ReactElement;
|
|
37
|
+
customFormSchema?: {
|
|
38
|
+
'new-card'?: ObjectSchema<any>;
|
|
39
|
+
'existing-card'?: ObjectSchema<any>;
|
|
40
|
+
};
|
|
41
|
+
customRender?: {
|
|
42
|
+
cardFormSection?: (props: CardFormSectionProps) => ReactElement;
|
|
43
|
+
savedCardsSection?: (props: IyzicoSavedCardsProps) => ReactElement;
|
|
44
|
+
installmentSection?: (props: InstallmentSectionProps) => ReactElement;
|
|
45
|
+
agreementAndSubmit?: (props: AgreementAndSubmitProps) => ReactElement;
|
|
46
|
+
};
|
|
47
|
+
formWrapperClassName?: string;
|
|
48
|
+
cardSelectionWrapperClassName?: string;
|
|
49
|
+
installmentWrapperClassName?: string;
|
|
50
|
+
formProps?: FormHTMLAttributes<HTMLFormElement>;
|
|
51
|
+
cardSelectionWrapperProps?: HTMLAttributes<HTMLDivElement>;
|
|
52
|
+
installmentWrapperProps?: HTMLAttributes<HTMLDivElement>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const defaultTranslations: IyzicoSavedCardOptionTexts = {
|
|
56
|
+
title: 'Pay with Iyzico',
|
|
57
|
+
button: 'Complete Payment',
|
|
58
|
+
installment: {
|
|
59
|
+
title: 'Installment Options'
|
|
60
|
+
},
|
|
61
|
+
errors: {
|
|
62
|
+
required: 'This field is required',
|
|
63
|
+
cardNumberLength: 'Card number must be 16 digits long.',
|
|
64
|
+
cvvLength: 'CVV must be 3 digits long.',
|
|
65
|
+
cardHolderMatches: 'Please enter a valid name',
|
|
66
|
+
saveCardRequired: 'Please select the Save My Card option'
|
|
67
|
+
},
|
|
68
|
+
label: {
|
|
69
|
+
cardHolder: 'Cardholder Name',
|
|
70
|
+
cardNumber: 'Card Number',
|
|
71
|
+
expiryDate: 'Expiration Date',
|
|
72
|
+
cvv: 'CVV',
|
|
73
|
+
saveCardForFuture: 'Save this card for future payments'
|
|
74
|
+
},
|
|
75
|
+
placeholder: {
|
|
76
|
+
cardHolder: 'Name on Card',
|
|
77
|
+
cardMonth: 'Month',
|
|
78
|
+
cardYear: 'Year'
|
|
79
|
+
},
|
|
80
|
+
savedCardsTitle: 'Select from your saved cards:',
|
|
81
|
+
paymentErrorMessage: 'An error occurred while processing your payment'
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const mergeTranslations = (
|
|
85
|
+
customTranslations: IyzicoSavedCardOptionTexts,
|
|
86
|
+
defaultTranslations: IyzicoSavedCardOptionTexts
|
|
87
|
+
) => {
|
|
88
|
+
return {
|
|
89
|
+
...defaultTranslations,
|
|
90
|
+
...customTranslations,
|
|
91
|
+
installment: {
|
|
92
|
+
...defaultTranslations.installment,
|
|
93
|
+
...customTranslations.installment
|
|
94
|
+
},
|
|
95
|
+
errors: {
|
|
96
|
+
...defaultTranslations.errors,
|
|
97
|
+
...customTranslations.errors
|
|
98
|
+
},
|
|
99
|
+
label: {
|
|
100
|
+
...defaultTranslations.label,
|
|
101
|
+
...customTranslations.label
|
|
102
|
+
},
|
|
103
|
+
placeholder: {
|
|
104
|
+
...defaultTranslations.placeholder,
|
|
105
|
+
...customTranslations.placeholder
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const createFormSchema = (
|
|
111
|
+
errors: ErrorTexts,
|
|
112
|
+
type: 'existing-card' | 'new-card'
|
|
113
|
+
) => {
|
|
114
|
+
const baseSchema = {
|
|
115
|
+
agreement: yup
|
|
116
|
+
.boolean()
|
|
117
|
+
.oneOf([true], errors?.required ?? defaultTranslations.errors.required),
|
|
118
|
+
register_consumer_card: yup.boolean().default(false)
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (type === 'new-card') {
|
|
122
|
+
return yup.object().shape({
|
|
123
|
+
...baseSchema,
|
|
124
|
+
card_holder: yup
|
|
125
|
+
.string()
|
|
126
|
+
.required(errors?.required ?? defaultTranslations.errors.required)
|
|
127
|
+
.matches(
|
|
128
|
+
/^[a-zA-ZğüşöçıİĞÜŞÖÇ\s]+$/,
|
|
129
|
+
errors?.cardHolderMatches ??
|
|
130
|
+
defaultTranslations.errors.cardHolderMatches
|
|
131
|
+
),
|
|
132
|
+
card_number: yup
|
|
133
|
+
.string()
|
|
134
|
+
.transform((value: string) => value.replace(/_/g, '').replace(/ /g, ''))
|
|
135
|
+
.length(
|
|
136
|
+
16,
|
|
137
|
+
errors?.cardNumberLength ??
|
|
138
|
+
defaultTranslations.errors.cardNumberLength
|
|
139
|
+
)
|
|
140
|
+
.required(errors?.required ?? defaultTranslations.errors.required),
|
|
141
|
+
card_month: yup
|
|
142
|
+
.string()
|
|
143
|
+
.required(errors?.required ?? defaultTranslations.errors.required),
|
|
144
|
+
card_year: yup
|
|
145
|
+
.string()
|
|
146
|
+
.required(errors?.required ?? defaultTranslations.errors.required),
|
|
147
|
+
card_cvv: yup
|
|
148
|
+
.string()
|
|
149
|
+
.transform((value: string) => value.replace(/_/g, '').replace(/ /g, ''))
|
|
150
|
+
.length(3, errors?.cvvLength ?? defaultTranslations.errors.cvvLength)
|
|
151
|
+
.required(errors?.required ?? defaultTranslations.errors.required)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return yup.object().shape(baseSchema);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const IyzicoSavedCardOption = ({
|
|
159
|
+
texts = defaultTranslations,
|
|
160
|
+
agreementCheckbox,
|
|
161
|
+
customFormSchema,
|
|
162
|
+
customRender,
|
|
163
|
+
formWrapperClassName = 'flex flex-wrap w-full',
|
|
164
|
+
cardSelectionWrapperClassName = 'w-full flex flex-col xl:w-6/10',
|
|
165
|
+
installmentWrapperClassName = 'w-full xl:w-4/10 xl:border-l xl:border-t-0',
|
|
166
|
+
formProps = {},
|
|
167
|
+
cardSelectionWrapperProps = {},
|
|
168
|
+
installmentWrapperProps = {}
|
|
169
|
+
}: IyzicoSavedCardOptionProps) => {
|
|
170
|
+
const mergedTexts = useMemo(
|
|
171
|
+
() => mergeTranslations(texts, defaultTranslations),
|
|
172
|
+
[texts]
|
|
173
|
+
);
|
|
174
|
+
const [completeSavedCard] = useCompleteSavedCardMutation();
|
|
175
|
+
const [setSavedCard] = useSetSavedCardMutation();
|
|
176
|
+
const installmentOptions = useAppSelector(
|
|
177
|
+
(state) => state.checkout.installmentOptions
|
|
178
|
+
);
|
|
179
|
+
const ucs = useAppSelector((state) => state.savedCard.ucs);
|
|
180
|
+
const [type, setType] = useState<'new-card' | 'existing-card'>('new-card');
|
|
181
|
+
const [months, setMonths] = useState([]);
|
|
182
|
+
const [years, setYears] = useState([]);
|
|
183
|
+
const [formError, setFormError] = useState(null);
|
|
184
|
+
|
|
185
|
+
const getFormSchema = () => {
|
|
186
|
+
if (customFormSchema?.[type]) {
|
|
187
|
+
return customFormSchema[type];
|
|
188
|
+
}
|
|
189
|
+
return createFormSchema(mergedTexts.errors, type);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const {
|
|
193
|
+
register,
|
|
194
|
+
handleSubmit,
|
|
195
|
+
control,
|
|
196
|
+
formState: { errors, isSubmitting },
|
|
197
|
+
setValue: setFormValue
|
|
198
|
+
} = useForm({
|
|
199
|
+
resolver: yupResolver(getFormSchema()) as any
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const onSubmit = async (data) => {
|
|
203
|
+
if (isSubmitting) return;
|
|
204
|
+
|
|
205
|
+
if (!window.iyziUcsInit) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setFormError(null);
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
if (type === 'existing-card') {
|
|
213
|
+
const res = await completeSavedCard({
|
|
214
|
+
agreement: data.agreement,
|
|
215
|
+
consumer_token: window.universalCardStorage.consumerToken,
|
|
216
|
+
card_token: window.universalCardStorage.cardToken,
|
|
217
|
+
register_consumer_card: true
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if ('data' in res && res.data?.errors) {
|
|
221
|
+
setFormError(res.data.errors);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
const res = await completeSavedCard({
|
|
225
|
+
agreement: data.agreement,
|
|
226
|
+
card_holder: data.card_holder,
|
|
227
|
+
card_number: data.card_number.replaceAll(' ', ''),
|
|
228
|
+
card_month: data.card_month,
|
|
229
|
+
card_year: data.card_year,
|
|
230
|
+
card_cvv: data.card_cvv,
|
|
231
|
+
register_consumer_card: data.register_consumer_card || false
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if ('data' in res && res.data?.errors) {
|
|
235
|
+
setFormError(res.data.errors);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('Error completing saved card:', error);
|
|
240
|
+
setFormError({
|
|
241
|
+
non_field_errors:
|
|
242
|
+
mergedTexts.paymentErrorMessage ??
|
|
243
|
+
defaultTranslations.paymentErrorMessage
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const ucsScript = ucs?.script && (
|
|
249
|
+
<Script
|
|
250
|
+
id="saved_card"
|
|
251
|
+
strategy="afterInteractive"
|
|
252
|
+
dangerouslySetInnerHTML={{ __html: ucs.script }}
|
|
253
|
+
/>
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
window.iyziUcsInit?.createTag();
|
|
258
|
+
|
|
259
|
+
const monthsList = [
|
|
260
|
+
{
|
|
261
|
+
label:
|
|
262
|
+
mergedTexts.placeholder?.cardMonth ??
|
|
263
|
+
defaultTranslations.placeholder.cardMonth,
|
|
264
|
+
value: ''
|
|
265
|
+
}
|
|
266
|
+
];
|
|
267
|
+
const yearsList = [
|
|
268
|
+
{
|
|
269
|
+
label:
|
|
270
|
+
mergedTexts.placeholder?.cardYear ??
|
|
271
|
+
defaultTranslations.placeholder.cardYear,
|
|
272
|
+
value: ''
|
|
273
|
+
}
|
|
274
|
+
];
|
|
275
|
+
const date = new Date();
|
|
276
|
+
const currentYear = date.getFullYear();
|
|
277
|
+
|
|
278
|
+
for (let i = 1; i <= 12; i++) {
|
|
279
|
+
monthsList.push({ label: i.toString(), value: i.toString() });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (let i = currentYear; i < currentYear + 13; i++) {
|
|
283
|
+
yearsList.push({ label: i.toString(), value: i.toString() });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
setMonths(monthsList);
|
|
287
|
+
setYears(yearsList);
|
|
288
|
+
|
|
289
|
+
if (window.iyziUcsInit?.ucsToken) {
|
|
290
|
+
setSavedCard({
|
|
291
|
+
card: window.iyziUcsInit.ucsToken
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}, []);
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (window.iyziUcsInit?.scriptType === 'CONSUMER_WITH_CARD_EXIST') {
|
|
298
|
+
if (window.universalCardStorage?.cardToken) {
|
|
299
|
+
setType('existing-card');
|
|
300
|
+
} else {
|
|
301
|
+
setType('new-card');
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
setType('new-card');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const checkboxInterval = setInterval(() => {
|
|
308
|
+
if (window.universalCardStorage?.cardToken !== undefined) {
|
|
309
|
+
setType('existing-card');
|
|
310
|
+
} else {
|
|
311
|
+
setType('new-card');
|
|
312
|
+
}
|
|
313
|
+
}, 100);
|
|
314
|
+
|
|
315
|
+
return () => clearInterval(checkboxInterval);
|
|
316
|
+
}, [type]);
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<>
|
|
320
|
+
<form
|
|
321
|
+
className={formWrapperClassName}
|
|
322
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
323
|
+
{...formProps}
|
|
324
|
+
>
|
|
325
|
+
<div
|
|
326
|
+
className={cardSelectionWrapperClassName}
|
|
327
|
+
{...cardSelectionWrapperProps}
|
|
328
|
+
>
|
|
329
|
+
{mergedTexts.title && (
|
|
330
|
+
<div className="border-solid border-gray-400 px-4 py-2">
|
|
331
|
+
<span className="text-black-800 text-lg font-medium">
|
|
332
|
+
{mergedTexts.title}
|
|
333
|
+
</span>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
{window.iyziUcsInit?.scriptType === 'CONSUMER_WITH_CARD_EXIST' && (
|
|
338
|
+
<>
|
|
339
|
+
{customRender?.savedCardsSection ? (
|
|
340
|
+
customRender.savedCardsSection({
|
|
341
|
+
title: mergedTexts.savedCardsTitle
|
|
342
|
+
})
|
|
343
|
+
) : (
|
|
344
|
+
<div>
|
|
345
|
+
<h3 className="text-sm font-medium text-gray-800 mb-3 px-4 sm:px-6">
|
|
346
|
+
{mergedTexts.savedCardsTitle}
|
|
347
|
+
</h3>
|
|
348
|
+
<div id="ucs-cards" className="mb-3 px-4 sm:px-6"></div>
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
</>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{type === 'new-card' && (
|
|
355
|
+
<>
|
|
356
|
+
{customRender?.cardFormSection ? (
|
|
357
|
+
customRender.cardFormSection({
|
|
358
|
+
register,
|
|
359
|
+
control,
|
|
360
|
+
errors,
|
|
361
|
+
months,
|
|
362
|
+
years,
|
|
363
|
+
translations: mergedTexts.label,
|
|
364
|
+
setFormValue
|
|
365
|
+
})
|
|
366
|
+
) : (
|
|
367
|
+
<CardFormSection
|
|
368
|
+
register={register}
|
|
369
|
+
control={control}
|
|
370
|
+
errors={errors}
|
|
371
|
+
months={months}
|
|
372
|
+
years={years}
|
|
373
|
+
translations={mergedTexts.label}
|
|
374
|
+
/>
|
|
375
|
+
)}
|
|
376
|
+
</>
|
|
377
|
+
)}
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div
|
|
381
|
+
className={installmentWrapperClassName}
|
|
382
|
+
{...installmentWrapperProps}
|
|
383
|
+
>
|
|
384
|
+
{customRender?.installmentSection ? (
|
|
385
|
+
customRender.installmentSection({
|
|
386
|
+
title: mergedTexts.installment?.title,
|
|
387
|
+
installmentOptions,
|
|
388
|
+
translations: mergedTexts.installment,
|
|
389
|
+
errors: errors.installment,
|
|
390
|
+
setFormValue
|
|
391
|
+
})
|
|
392
|
+
) : (
|
|
393
|
+
<InstallmentSection
|
|
394
|
+
title={mergedTexts.installment?.title}
|
|
395
|
+
installmentOptions={installmentOptions}
|
|
396
|
+
translations={mergedTexts.installment}
|
|
397
|
+
errors={errors.installment}
|
|
398
|
+
/>
|
|
399
|
+
)}
|
|
400
|
+
|
|
401
|
+
<div className="flex flex-col text-xs">
|
|
402
|
+
{customRender?.agreementAndSubmit ? (
|
|
403
|
+
customRender.agreementAndSubmit({
|
|
404
|
+
agreementCheckbox,
|
|
405
|
+
control,
|
|
406
|
+
errors,
|
|
407
|
+
buttonText: mergedTexts.button,
|
|
408
|
+
formError,
|
|
409
|
+
isSubmitting
|
|
410
|
+
})
|
|
411
|
+
) : (
|
|
412
|
+
<AgreementAndSubmit
|
|
413
|
+
agreementCheckbox={agreementCheckbox}
|
|
414
|
+
control={control}
|
|
415
|
+
errors={errors}
|
|
416
|
+
buttonText={mergedTexts.button}
|
|
417
|
+
formError={formError}
|
|
418
|
+
isSubmitting={isSubmitting}
|
|
419
|
+
/>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</form>
|
|
424
|
+
{ucsScript}
|
|
425
|
+
</>
|
|
426
|
+
);
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
export default IyzicoSavedCardOption;
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import * as yup from 'yup';
|
|
4
4
|
import { yupResolver } from '@hookform/resolvers/yup';
|
|
5
5
|
import { useForm } from 'react-hook-form';
|
|
6
|
-
import React, { ReactElement, useMemo } from 'react';
|
|
6
|
+
import React, { ReactElement, useMemo, useState } from 'react';
|
|
7
7
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
8
|
+
import { checkPaymentWillRedirect } from '@akinon/next/utils';
|
|
8
9
|
import {
|
|
9
10
|
useCompleteSavedCardMutation,
|
|
10
11
|
useSetSavedCardMutation
|
|
@@ -95,7 +96,7 @@ const SavedCardOption = ({
|
|
|
95
96
|
customRender,
|
|
96
97
|
formWrapperClassName = 'flex flex-wrap w-full',
|
|
97
98
|
cardSelectionWrapperClassName = 'w-full flex flex-col xl:w-6/10',
|
|
98
|
-
installmentWrapperClassName = 'w-full xl:w-4/10 xl:border-l xl:border-t-0
|
|
99
|
+
installmentWrapperClassName = 'w-full xl:w-4/10 xl:border-l xl:border-t-0',
|
|
99
100
|
formProps = {},
|
|
100
101
|
cardSelectionWrapperProps = {},
|
|
101
102
|
installmentWrapperProps = {}
|
|
@@ -105,7 +106,9 @@ const SavedCardOption = ({
|
|
|
105
106
|
[texts]
|
|
106
107
|
);
|
|
107
108
|
const dispatch = useAppDispatch();
|
|
108
|
-
const [
|
|
109
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
110
|
+
const [completeSavedCard, { isLoading: isCompleting }] =
|
|
111
|
+
useCompleteSavedCardMutation();
|
|
109
112
|
const [setSavedCard] = useSetSavedCardMutation();
|
|
110
113
|
const installmentOptions = useAppSelector(
|
|
111
114
|
(state) => state.checkout.installmentOptions
|
|
@@ -116,11 +119,11 @@ const SavedCardOption = ({
|
|
|
116
119
|
register,
|
|
117
120
|
handleSubmit,
|
|
118
121
|
control,
|
|
119
|
-
formState: { errors },
|
|
122
|
+
formState: { errors, isSubmitting },
|
|
120
123
|
setValue,
|
|
121
124
|
watch
|
|
122
125
|
} = useForm({
|
|
123
|
-
resolver: yupResolver(createFormSchema(mergedTexts.errors))
|
|
126
|
+
resolver: yupResolver(createFormSchema(mergedTexts.errors)) as any
|
|
124
127
|
});
|
|
125
128
|
|
|
126
129
|
const selectedCardToken = watch('card');
|
|
@@ -129,15 +132,26 @@ const SavedCardOption = ({
|
|
|
129
132
|
[cards, selectedCardToken]
|
|
130
133
|
);
|
|
131
134
|
|
|
135
|
+
const isButtonDisabled = isSubmitting || isCompleting || isProcessing;
|
|
136
|
+
|
|
132
137
|
const handleCardSelection = async (card) => {
|
|
133
138
|
await setSavedCard({ card }).unwrap();
|
|
134
139
|
setValue('card', card.token, { shouldValidate: true });
|
|
135
140
|
};
|
|
136
141
|
|
|
137
142
|
const onSubmit = async () => {
|
|
143
|
+
if (isButtonDisabled) return;
|
|
144
|
+
|
|
145
|
+
setIsProcessing(true);
|
|
146
|
+
|
|
138
147
|
try {
|
|
139
|
-
await completeSavedCard({ agreement: true });
|
|
148
|
+
const response = await completeSavedCard({ agreement: true }).unwrap();
|
|
149
|
+
|
|
150
|
+
if (response?.errors || !checkPaymentWillRedirect(response)) {
|
|
151
|
+
setIsProcessing(false);
|
|
152
|
+
}
|
|
140
153
|
} catch (error) {
|
|
154
|
+
setIsProcessing(false);
|
|
141
155
|
console.error('Error completing saved card:', error);
|
|
142
156
|
}
|
|
143
157
|
};
|
|
@@ -183,7 +197,6 @@ const SavedCardOption = ({
|
|
|
183
197
|
{customRender?.installmentSection ? (
|
|
184
198
|
customRender.installmentSection({
|
|
185
199
|
title: mergedTexts.installment?.title,
|
|
186
|
-
selectedCard,
|
|
187
200
|
installmentOptions,
|
|
188
201
|
translations: mergedTexts.installment,
|
|
189
202
|
errors: errors.installment
|
|
@@ -191,7 +204,6 @@ const SavedCardOption = ({
|
|
|
191
204
|
) : (
|
|
192
205
|
<InstallmentSection
|
|
193
206
|
title={mergedTexts.installment?.title}
|
|
194
|
-
selectedCard={selectedCard}
|
|
195
207
|
installmentOptions={installmentOptions}
|
|
196
208
|
translations={mergedTexts.installment}
|
|
197
209
|
errors={errors.installment}
|
|
@@ -203,7 +215,8 @@ const SavedCardOption = ({
|
|
|
203
215
|
agreementCheckbox,
|
|
204
216
|
control,
|
|
205
217
|
errors,
|
|
206
|
-
buttonText: mergedTexts.button
|
|
218
|
+
buttonText: mergedTexts.button,
|
|
219
|
+
isSubmitting: isButtonDisabled
|
|
207
220
|
})
|
|
208
221
|
) : (
|
|
209
222
|
<AgreementAndSubmit
|
|
@@ -211,6 +224,7 @@ const SavedCardOption = ({
|
|
|
211
224
|
control={control}
|
|
212
225
|
errors={errors}
|
|
213
226
|
buttonText={mergedTexts.button}
|
|
227
|
+
isSubmitting={isButtonDisabled}
|
|
214
228
|
/>
|
|
215
229
|
)}
|
|
216
230
|
</div>
|