@akinon/pz-saved-card 1.67.0 → 1.68.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/CHANGELOG.md +6 -9
- package/assets/img/amex.jpg +0 -0
- package/assets/img/mastercard.png +0 -0
- package/assets/img/other.png +0 -0
- package/assets/img/troy.png +0 -0
- package/assets/img/visa.png +0 -0
- package/package.json +6 -4
- package/readme.md +36 -27
- package/src/components/agreement-and-submit.tsx +25 -0
- package/src/components/card-label.tsx +17 -0
- package/src/components/card-selection-section.tsx +51 -0
- package/src/components/delete-confirmation-modal.tsx +80 -0
- package/src/components/delete-icon.tsx +8 -0
- package/src/components/error-text.tsx +7 -0
- package/src/components/installment-section.tsx +22 -0
- package/src/components/installments.tsx +113 -0
- package/src/index.tsx +2 -4
- package/src/{endpoints.ts → redux/api.ts} +65 -23
- package/src/redux/reducer.ts +25 -13
- package/src/types/index.ts +50 -16
- package/src/utils/index.ts +15 -0
- package/src/views/saved-card-option.tsx +232 -0
- package/src/redux/middleware.ts +0 -25
- package/src/routes/saved-card-redirect.tsx +0 -25
- package/src/views/installments.tsx +0 -108
- package/src/views/option.tsx +0 -400
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
# @akinon/pz-saved-card
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.68.0
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Minor Changes
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## 1.63.0
|
|
12
|
-
|
|
13
|
-
## 1.62.0
|
|
7
|
+
- b92001c: ZERO-2903: fix missing dependency in useEffect hook
|
|
8
|
+
- 63597bc: ZERO-2903: update saved card readme
|
|
9
|
+
- 8fb37c4: ZERO-2903: enchance SavedCardOption component with custom wrapper props and classnames
|
|
10
|
+
- ce25dac: ZERO-2903: optimize saved card
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-saved-card",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"peerDependencies": {
|
|
7
7
|
"react": "^18.0.0",
|
|
8
8
|
"react-dom": "^18.0.0"
|
|
9
9
|
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"react-redux": "8.1.3"
|
|
12
|
+
},
|
|
10
13
|
"devDependencies": {
|
|
11
14
|
"@types/node": "^18.7.8",
|
|
12
15
|
"@types/react": "^18.0.17",
|
|
13
16
|
"@types/react-dom": "^18.0.6",
|
|
17
|
+
"prettier": "^3.0.3",
|
|
14
18
|
"react": "^18.2.0",
|
|
15
19
|
"react-dom": "^18.2.0",
|
|
16
|
-
"typescript": "^
|
|
17
|
-
"react-hook-form": "7.31.3",
|
|
18
|
-
"@hookform/resolvers": "2.9.0"
|
|
20
|
+
"typescript": "^5.2.2"
|
|
19
21
|
}
|
|
20
22
|
}
|
package/readme.md
CHANGED
|
@@ -1,36 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Saved Card Plugin
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
##### File Path: src/views/checkout/steps/payment/options/saved-card.tsx
|
|
3
|
+
## Installation
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export default function SavedCard() {
|
|
11
|
-
return (
|
|
12
|
-
<PluginModule
|
|
13
|
-
component={Component.SavedCard}
|
|
14
|
-
props={{
|
|
15
|
-
agreementCheckbox: (
|
|
16
|
-
<CheckoutAgreements control={null} fieldId="agreement" error={null} />
|
|
17
|
-
)
|
|
18
|
-
}}
|
|
19
|
-
/>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
5
|
+
There are two ways to install the Saved Card plugin:
|
|
6
|
+
|
|
7
|
+
### 1. Install the npm package using Yarn
|
|
22
8
|
|
|
9
|
+
For the latest version, you can install the package using Yarn:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
yarn add @akinon/pz-saved-card
|
|
23
13
|
```
|
|
24
14
|
|
|
25
|
-
|
|
15
|
+
### 2. Preferred installation method
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
import { SavedCardRedirect } from 'pz-saved-card/src/routes/saved-card-redirect';
|
|
17
|
+
You can also use the following command to install the extension with the latest plugins:
|
|
29
18
|
|
|
30
|
-
|
|
19
|
+
```bash
|
|
20
|
+
npx @akinon/projectzero@latest --plugins
|
|
31
21
|
```
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
##### File Path: src/views/checkout/steps/payment/options/saved-card.tsx
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
29
|
+
|
|
30
|
+
const SavedCard = () => {
|
|
31
|
+
return (
|
|
32
|
+
<PluginModule
|
|
33
|
+
component={Component.SavedCard}
|
|
34
|
+
props={{
|
|
35
|
+
texts: {
|
|
36
|
+
title: 'Pay with Saved Card',
|
|
37
|
+
button: 'Pay Now'
|
|
38
|
+
}
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default SavedCard;
|
|
45
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { cloneElement } from 'react';
|
|
2
|
+
import { Button, Icon } from '@akinon/next/components';
|
|
3
|
+
|
|
4
|
+
export const AgreementAndSubmit = ({
|
|
5
|
+
agreementCheckbox,
|
|
6
|
+
control,
|
|
7
|
+
errors,
|
|
8
|
+
buttonText
|
|
9
|
+
}) => (
|
|
10
|
+
<div className="flex flex-col text-xs pb-4 px-4 sm:px-6">
|
|
11
|
+
{agreementCheckbox &&
|
|
12
|
+
cloneElement(agreementCheckbox, {
|
|
13
|
+
control,
|
|
14
|
+
error: errors.agreement,
|
|
15
|
+
fieldId: 'agreement'
|
|
16
|
+
})}
|
|
17
|
+
<Button
|
|
18
|
+
type="submit"
|
|
19
|
+
className="group uppercase mt-4 inline-flex items-center justify-center"
|
|
20
|
+
>
|
|
21
|
+
<span>{buttonText}</span>
|
|
22
|
+
<Icon name="chevron-end" size={12} className="ml-2 h-3" />
|
|
23
|
+
</Button>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getCreditCardType } from '../utils';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { cardImages } from '../views/saved-card-option';
|
|
4
|
+
import { Image } from '@akinon/next/components';
|
|
5
|
+
|
|
6
|
+
export const CardLabel = ({ card }) => (
|
|
7
|
+
<label className="flex flex-col w-full cursor-pointer md:flex-row md:items-center md:justify-between">
|
|
8
|
+
<p className="w-full text-[10px] lg:w-1/3">{card.masked_card_number}</p>
|
|
9
|
+
<Image
|
|
10
|
+
className="w-8 h-6 object-contain flex items-center justify-center"
|
|
11
|
+
width={50}
|
|
12
|
+
height={50}
|
|
13
|
+
src={cardImages[getCreditCardType(card.masked_card_number)].src}
|
|
14
|
+
alt={card.name}
|
|
15
|
+
/>
|
|
16
|
+
</label>
|
|
17
|
+
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { setDeletionModalId, setDeletionModalVisible } from '../redux/reducer';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ErrorText } from './error-text';
|
|
4
|
+
import { CardLabel } from './card-label';
|
|
5
|
+
import { DeleteIcon } from './delete-icon';
|
|
6
|
+
|
|
7
|
+
export const CardSelectionSection = ({
|
|
8
|
+
title,
|
|
9
|
+
cards,
|
|
10
|
+
selectedCard,
|
|
11
|
+
onSelect,
|
|
12
|
+
register,
|
|
13
|
+
errors,
|
|
14
|
+
dispatch
|
|
15
|
+
}) => (
|
|
16
|
+
<div className="border-solid border-gray-400 px-4 py-2">
|
|
17
|
+
<span className="text-black-800 text-lg font-medium">{title}</span>
|
|
18
|
+
<ul className="mt-4 text-xs w-full">
|
|
19
|
+
{cards?.map((card) => (
|
|
20
|
+
<li
|
|
21
|
+
key={card.token}
|
|
22
|
+
className="p-4 mb-2 border-2 border-gray-200 flex justify-between items-center cursor-pointer"
|
|
23
|
+
onClick={(e) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
onSelect(card);
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<input
|
|
29
|
+
name="card"
|
|
30
|
+
type="radio"
|
|
31
|
+
checked={selectedCard?.token === card.token}
|
|
32
|
+
value={card.token}
|
|
33
|
+
id={card.token}
|
|
34
|
+
className="mr-2"
|
|
35
|
+
onChange={() => {}}
|
|
36
|
+
{...register('card')}
|
|
37
|
+
/>
|
|
38
|
+
<CardLabel card={card} />
|
|
39
|
+
<DeleteIcon
|
|
40
|
+
onClick={(e) => {
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
dispatch(setDeletionModalId(card.id));
|
|
43
|
+
dispatch(setDeletionModalVisible(true));
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
</li>
|
|
47
|
+
))}
|
|
48
|
+
</ul>
|
|
49
|
+
{errors.card && <ErrorText message={errors.card?.message} />}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button, LoaderSpinner, Modal } from '@akinon/next/components';
|
|
4
|
+
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
5
|
+
import {
|
|
6
|
+
setCards,
|
|
7
|
+
setDeletionModalId,
|
|
8
|
+
setDeletionModalVisible
|
|
9
|
+
} from '../redux/reducer';
|
|
10
|
+
import { useDeleteSavedCardMutation } from '../redux/api';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { DeletePopupTexts } from '../types';
|
|
13
|
+
|
|
14
|
+
const defaultTranslations = {
|
|
15
|
+
title: 'Are you sure you want to delete this card?',
|
|
16
|
+
delete_button: 'Delete',
|
|
17
|
+
cancel_button: 'Cancel'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface DeleteConfirmationModalProps {
|
|
21
|
+
translations?: DeletePopupTexts;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DeleteConfirmationModal = ({
|
|
25
|
+
translations
|
|
26
|
+
}: DeleteConfirmationModalProps) => {
|
|
27
|
+
const [error, setError] = useState<string | null>(null);
|
|
28
|
+
const { cards, deletion } = useAppSelector((state) => state.savedCard);
|
|
29
|
+
const dispatch = useAppDispatch();
|
|
30
|
+
|
|
31
|
+
const [deleteCard, { isLoading }] = useDeleteSavedCardMutation();
|
|
32
|
+
|
|
33
|
+
const handleClose = () => {
|
|
34
|
+
dispatch(setDeletionModalId(null));
|
|
35
|
+
dispatch(setDeletionModalVisible(false));
|
|
36
|
+
setError(null);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleDelete = async () => {
|
|
40
|
+
try {
|
|
41
|
+
await deleteCard(deletion.id).unwrap();
|
|
42
|
+
dispatch(setCards(cards.filter((card) => card.id !== deletion.id)));
|
|
43
|
+
handleClose();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
setError(error.data.detail);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Modal
|
|
51
|
+
portalId="saved-card-remove-card-modal"
|
|
52
|
+
className="w-full sm:w-[28rem] max-h-[90vh] overflow-y-auto"
|
|
53
|
+
open={deletion.isModalVisible}
|
|
54
|
+
setOpen={() => dispatch(setDeletionModalVisible(false))}
|
|
55
|
+
>
|
|
56
|
+
<div className="px-6">
|
|
57
|
+
<h3 className="text-center mt-4 text-lg">
|
|
58
|
+
{translations?.title ?? defaultTranslations.title}
|
|
59
|
+
</h3>
|
|
60
|
+
<div className="flex flex-col gap-3 p-5 w-3/4 m-auto">
|
|
61
|
+
<Button className="py-3 h-auto" onClick={handleDelete}>
|
|
62
|
+
{isLoading ? (
|
|
63
|
+
<LoaderSpinner className="w-4 h-4" />
|
|
64
|
+
) : (
|
|
65
|
+
translations?.delete_button ?? defaultTranslations.delete_button
|
|
66
|
+
)}
|
|
67
|
+
</Button>
|
|
68
|
+
<Button
|
|
69
|
+
appearance="outlined"
|
|
70
|
+
className="underline px-5 py-3 h-auto"
|
|
71
|
+
onClick={handleClose}
|
|
72
|
+
>
|
|
73
|
+
{translations?.cancel_button ?? defaultTranslations.cancel_button}
|
|
74
|
+
</Button>
|
|
75
|
+
{error && <p className="text-error text-xs text-center">{error}</p>}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</Modal>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import SavedCardInstallments from './installments';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
export const InstallmentSection = ({
|
|
5
|
+
title,
|
|
6
|
+
selectedCard,
|
|
7
|
+
installmentOptions,
|
|
8
|
+
translations,
|
|
9
|
+
errors
|
|
10
|
+
}) => (
|
|
11
|
+
<div className="border-solid border-gray-400 bg-white">
|
|
12
|
+
<div className="px-4 py-2">
|
|
13
|
+
<span className="text-black-800 text-lg font-medium">{title}</span>
|
|
14
|
+
</div>
|
|
15
|
+
<SavedCardInstallments
|
|
16
|
+
selectedCard={selectedCard}
|
|
17
|
+
installmentOptions={installmentOptions}
|
|
18
|
+
translations={translations}
|
|
19
|
+
error={errors}
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Price, Radio } from '@akinon/next/components';
|
|
2
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
3
|
+
import { useSetSavedCardInstallmentOptionMutation } from '../redux/api';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import { SavedCard } from '../redux/reducer';
|
|
6
|
+
import { InstallmentTexts } from '../types';
|
|
7
|
+
|
|
8
|
+
const defaultTranslations = {
|
|
9
|
+
payments: 'Payments',
|
|
10
|
+
per_month: 'Per Month',
|
|
11
|
+
total: 'Total'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type SavedCardInstallmentsProps = {
|
|
15
|
+
selectedCard: SavedCard;
|
|
16
|
+
installmentOptions: any[];
|
|
17
|
+
translations?: InstallmentTexts;
|
|
18
|
+
error: any;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const SavedCardInstallments = ({
|
|
22
|
+
selectedCard,
|
|
23
|
+
installmentOptions = [],
|
|
24
|
+
translations,
|
|
25
|
+
error
|
|
26
|
+
}: SavedCardInstallmentsProps) => {
|
|
27
|
+
const { t } = useLocalization();
|
|
28
|
+
const [installmentOption, setInstallmentOption] = useState(null);
|
|
29
|
+
const [setInstallment] = useSetSavedCardInstallmentOptionMutation();
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (
|
|
33
|
+
selectedCard &&
|
|
34
|
+
installmentOptions.length > 0 &&
|
|
35
|
+
installmentOptions[0]?.pk !== installmentOption
|
|
36
|
+
) {
|
|
37
|
+
const firstOptionPk = installmentOptions[0].pk;
|
|
38
|
+
setInstallment({
|
|
39
|
+
installment: firstOptionPk
|
|
40
|
+
});
|
|
41
|
+
setInstallmentOption(firstOptionPk);
|
|
42
|
+
}
|
|
43
|
+
}, [installmentOptions, installmentOption, setInstallment, selectedCard]);
|
|
44
|
+
|
|
45
|
+
if (installmentOptions.length === 0) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="text-xs text-black-800 p-4 sm:p-6">
|
|
48
|
+
{t('checkout.payment.installment_options.description')}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<div className="px-4 mb-4 sm:px-6 sm:mb-6">
|
|
56
|
+
<table className="w-full border border-solid border-gray-400">
|
|
57
|
+
<thead>
|
|
58
|
+
<tr>
|
|
59
|
+
<th className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 text-start">
|
|
60
|
+
{translations?.payments || defaultTranslations.payments}
|
|
61
|
+
</th>
|
|
62
|
+
<th className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 text-right">
|
|
63
|
+
{translations?.per_month || defaultTranslations.per_month}
|
|
64
|
+
</th>
|
|
65
|
+
<th className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 text-right">
|
|
66
|
+
{translations?.total || defaultTranslations.total}
|
|
67
|
+
</th>
|
|
68
|
+
</tr>
|
|
69
|
+
</thead>
|
|
70
|
+
<tbody>
|
|
71
|
+
{installmentOptions.map((option) => (
|
|
72
|
+
<tr
|
|
73
|
+
key={`installment-${option.pk}`}
|
|
74
|
+
className="border-t border-solid border-gray-400"
|
|
75
|
+
>
|
|
76
|
+
<td className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 text-left">
|
|
77
|
+
<Radio
|
|
78
|
+
value={option.pk}
|
|
79
|
+
name="installment"
|
|
80
|
+
checked={option.pk === installmentOption}
|
|
81
|
+
onChange={() => {
|
|
82
|
+
setInstallment({
|
|
83
|
+
installment: option.pk
|
|
84
|
+
});
|
|
85
|
+
setInstallmentOption(option.pk);
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<span className="w-full flex items-center justify-start pl-2">
|
|
89
|
+
<span className="text-xs text-black-800 transition-all">
|
|
90
|
+
{option.label}
|
|
91
|
+
</span>
|
|
92
|
+
</span>
|
|
93
|
+
</Radio>
|
|
94
|
+
</td>
|
|
95
|
+
<td className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 text-right">
|
|
96
|
+
<Price value={option.monthly_price_with_accrued_interest} />
|
|
97
|
+
</td>
|
|
98
|
+
<td className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 text-right">
|
|
99
|
+
<Price value={option.price_with_accrued_interest} />
|
|
100
|
+
</td>
|
|
101
|
+
</tr>
|
|
102
|
+
))}
|
|
103
|
+
</tbody>
|
|
104
|
+
</table>
|
|
105
|
+
</div>
|
|
106
|
+
{error && (
|
|
107
|
+
<div className="px-6 mt-4 text-sm text-error">{error.message}</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default SavedCardInstallments;
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export * from './views/option';
|
|
2
|
-
|
|
3
1
|
import savedCardReducer from './redux/reducer';
|
|
4
|
-
import
|
|
2
|
+
import SavedCardOption from './views/saved-card-option';
|
|
5
3
|
|
|
6
|
-
export { savedCardReducer,
|
|
4
|
+
export { savedCardReducer, SavedCardOption };
|
|
@@ -2,6 +2,7 @@ import { CheckoutContext, PreOrder } from '@akinon/next/types';
|
|
|
2
2
|
import { api } from '@akinon/next/data/client/api';
|
|
3
3
|
import { buildClientRequestUrl } from '@akinon/next/utils';
|
|
4
4
|
import { setPaymentStepBusy } from '@akinon/next/redux/reducers/checkout';
|
|
5
|
+
import { SavedCard } from './reducer';
|
|
5
6
|
|
|
6
7
|
interface CheckoutResponse {
|
|
7
8
|
pre_order?: PreOrder;
|
|
@@ -13,21 +14,55 @@ interface CheckoutResponse {
|
|
|
13
14
|
redirect_url?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
interface GetResponse<T> {
|
|
18
|
+
count: number;
|
|
19
|
+
next: null;
|
|
20
|
+
previous: null;
|
|
21
|
+
results: T[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type SetSavedCardRequest = {
|
|
25
|
+
card: SavedCard;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type DeleteSavedCardRequest = number;
|
|
29
|
+
|
|
30
|
+
type SetInstallmentRequest = {
|
|
31
|
+
installment: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type CompleteSavedCardRequest = {
|
|
35
|
+
agreement: boolean;
|
|
36
|
+
};
|
|
26
37
|
|
|
27
38
|
export const savedCardApi = api.injectEndpoints({
|
|
28
39
|
endpoints: (build) => ({
|
|
29
|
-
|
|
30
|
-
query: (
|
|
40
|
+
getSavedCards: build.query<GetResponse<SavedCard>, void>({
|
|
41
|
+
query: () => ({
|
|
42
|
+
url: buildClientRequestUrl('/users/saved-cards/')
|
|
43
|
+
}),
|
|
44
|
+
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
|
45
|
+
dispatch(setPaymentStepBusy(true));
|
|
46
|
+
await queryFulfilled;
|
|
47
|
+
dispatch(setPaymentStepBusy(false));
|
|
48
|
+
}
|
|
49
|
+
}),
|
|
50
|
+
deleteSavedCard: build.mutation<
|
|
51
|
+
GetResponse<SavedCard>,
|
|
52
|
+
DeleteSavedCardRequest
|
|
53
|
+
>({
|
|
54
|
+
query: (cardId) => ({
|
|
55
|
+
url: buildClientRequestUrl(`/users/saved-cards/${cardId}`),
|
|
56
|
+
method: 'DELETE'
|
|
57
|
+
}),
|
|
58
|
+
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
|
59
|
+
dispatch(setPaymentStepBusy(true));
|
|
60
|
+
await queryFulfilled;
|
|
61
|
+
dispatch(setPaymentStepBusy(false));
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
setSavedCard: build.mutation<CheckoutResponse, SetSavedCardRequest>({
|
|
65
|
+
query: ({ card }) => ({
|
|
31
66
|
url: buildClientRequestUrl(
|
|
32
67
|
`/orders/checkout?page=SavedCardSelectionPage`,
|
|
33
68
|
{
|
|
@@ -36,17 +71,20 @@ export const savedCardApi = api.injectEndpoints({
|
|
|
36
71
|
),
|
|
37
72
|
method: 'POST',
|
|
38
73
|
body: {
|
|
39
|
-
card:
|
|
74
|
+
card: card.token
|
|
40
75
|
}
|
|
41
76
|
}),
|
|
42
|
-
async onQueryStarted(
|
|
77
|
+
async onQueryStarted({ card }, { dispatch, queryFulfilled }) {
|
|
43
78
|
dispatch(setPaymentStepBusy(true));
|
|
44
79
|
await queryFulfilled;
|
|
45
80
|
dispatch(setPaymentStepBusy(false));
|
|
46
81
|
}
|
|
47
82
|
}),
|
|
48
|
-
|
|
49
|
-
|
|
83
|
+
setSavedCardInstallmentOption: build.mutation<
|
|
84
|
+
CheckoutResponse,
|
|
85
|
+
SetInstallmentRequest
|
|
86
|
+
>({
|
|
87
|
+
query: ({ installment }) => ({
|
|
50
88
|
url: buildClientRequestUrl(
|
|
51
89
|
`/orders/checkout?page=SavedCardInstallmentSelectionPage`,
|
|
52
90
|
{
|
|
@@ -55,7 +93,7 @@ export const savedCardApi = api.injectEndpoints({
|
|
|
55
93
|
),
|
|
56
94
|
method: 'POST',
|
|
57
95
|
body: {
|
|
58
|
-
installment
|
|
96
|
+
installment
|
|
59
97
|
}
|
|
60
98
|
}),
|
|
61
99
|
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
|
@@ -64,19 +102,21 @@ export const savedCardApi = api.injectEndpoints({
|
|
|
64
102
|
dispatch(setPaymentStepBusy(false));
|
|
65
103
|
}
|
|
66
104
|
}),
|
|
67
|
-
|
|
105
|
+
completeSavedCard: build.mutation<
|
|
68
106
|
CheckoutResponse,
|
|
69
107
|
CompleteSavedCardRequest
|
|
70
108
|
>({
|
|
71
|
-
query: (
|
|
109
|
+
query: ({ agreement }) => ({
|
|
72
110
|
url: buildClientRequestUrl(
|
|
73
|
-
`/orders/checkout?page=
|
|
111
|
+
`/orders/checkout?page=CompleteSavedCardRequest`,
|
|
74
112
|
{
|
|
75
113
|
useFormData: true
|
|
76
114
|
}
|
|
77
115
|
),
|
|
78
116
|
method: 'POST',
|
|
79
|
-
body:
|
|
117
|
+
body: {
|
|
118
|
+
agreement
|
|
119
|
+
}
|
|
80
120
|
}),
|
|
81
121
|
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
|
82
122
|
dispatch(setPaymentStepBusy(true));
|
|
@@ -88,7 +128,9 @@ export const savedCardApi = api.injectEndpoints({
|
|
|
88
128
|
});
|
|
89
129
|
|
|
90
130
|
export const {
|
|
131
|
+
useGetSavedCardsQuery,
|
|
91
132
|
useSetSavedCardMutation,
|
|
92
|
-
|
|
93
|
-
|
|
133
|
+
useSetSavedCardInstallmentOptionMutation,
|
|
134
|
+
useCompleteSavedCardMutation,
|
|
135
|
+
useDeleteSavedCardMutation
|
|
94
136
|
} = savedCardApi;
|
package/src/redux/reducer.ts
CHANGED
|
@@ -1,33 +1,45 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
1
|
import { createSlice } from '@reduxjs/toolkit';
|
|
4
2
|
|
|
3
|
+
export type SavedCard = {
|
|
4
|
+
id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
masked_card_number: string;
|
|
7
|
+
token: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
5
10
|
export interface SavedCardState {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
ucsToken?: string;
|
|
11
|
+
cards?: Array<SavedCard>;
|
|
12
|
+
deletion: {
|
|
13
|
+
id: number;
|
|
14
|
+
isModalVisible: boolean;
|
|
11
15
|
};
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
const initialState: SavedCardState = {
|
|
15
|
-
|
|
19
|
+
cards: undefined,
|
|
20
|
+
deletion: {
|
|
21
|
+
id: null,
|
|
22
|
+
isModalVisible: false
|
|
23
|
+
}
|
|
16
24
|
};
|
|
17
25
|
|
|
18
26
|
const savedCardSlice = createSlice({
|
|
19
27
|
name: 'savedCard',
|
|
20
28
|
initialState,
|
|
21
29
|
reducers: {
|
|
22
|
-
|
|
23
|
-
state.
|
|
30
|
+
setCards(state, { payload }) {
|
|
31
|
+
state.cards = payload;
|
|
32
|
+
},
|
|
33
|
+
setDeletionModalId(state, { payload }) {
|
|
34
|
+
state.deletion.id = payload;
|
|
24
35
|
},
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
setDeletionModalVisible(state, { payload }) {
|
|
37
|
+
state.deletion.isModalVisible = payload;
|
|
27
38
|
}
|
|
28
39
|
}
|
|
29
40
|
});
|
|
30
41
|
|
|
31
|
-
export const {
|
|
42
|
+
export const { setCards, setDeletionModalId, setDeletionModalVisible } =
|
|
43
|
+
savedCardSlice.actions;
|
|
32
44
|
|
|
33
45
|
export default savedCardSlice.reducer;
|