@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/src/types/index.ts
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
1
|
-
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
export type InstallmentTexts = {
|
|
4
|
+
title?: string;
|
|
5
|
+
payments?: string;
|
|
6
|
+
per_month?: string;
|
|
7
|
+
total?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type DeletePopupTexts = {
|
|
11
|
+
title?: string;
|
|
12
|
+
delete_button?: string;
|
|
13
|
+
cancel_button?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ErrorTexts = {
|
|
17
|
+
required?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SavedCardOptionTexts = {
|
|
21
|
+
title?: string;
|
|
22
|
+
button?: string;
|
|
23
|
+
installment?: InstallmentTexts;
|
|
24
|
+
deletePopup?: DeletePopupTexts;
|
|
25
|
+
errors?: ErrorTexts;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type CardSelectionSectionProps = {
|
|
29
|
+
title: string;
|
|
30
|
+
cards: any[];
|
|
31
|
+
selectedCard: any;
|
|
32
|
+
onSelect: (card: any) => void;
|
|
33
|
+
register: any;
|
|
34
|
+
errors: any;
|
|
35
|
+
dispatch: any;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type InstallmentSectionProps = {
|
|
39
|
+
title: string;
|
|
40
|
+
selectedCard: any;
|
|
41
|
+
installmentOptions: any;
|
|
42
|
+
translations: InstallmentTexts;
|
|
43
|
+
errors: any;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type AgreementAndSubmitProps = {
|
|
47
|
+
agreementCheckbox: ReactElement | undefined;
|
|
48
|
+
control: any;
|
|
49
|
+
errors: any;
|
|
50
|
+
buttonText: string;
|
|
51
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const getCreditCardType = (maskedCardNumber: string): string => {
|
|
2
|
+
const cardNumber = maskedCardNumber.replace(/\D/g, '');
|
|
3
|
+
|
|
4
|
+
if (/^4/.test(cardNumber)) {
|
|
5
|
+
return 'visa';
|
|
6
|
+
} else if (/^5[1-5]/.test(cardNumber)) {
|
|
7
|
+
return 'mastercard';
|
|
8
|
+
} else if (/^3[47]/.test(cardNumber)) {
|
|
9
|
+
return 'amex';
|
|
10
|
+
} else if (/^9/.test(cardNumber)) {
|
|
11
|
+
return 'troy';
|
|
12
|
+
} else {
|
|
13
|
+
return 'other';
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as yup from 'yup';
|
|
4
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
5
|
+
import { useForm } from 'react-hook-form';
|
|
6
|
+
import React, { ReactElement, useEffect, useMemo } from 'react';
|
|
7
|
+
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
8
|
+
import {
|
|
9
|
+
useCompleteSavedCardMutation,
|
|
10
|
+
useGetSavedCardsQuery,
|
|
11
|
+
useSetSavedCardMutation
|
|
12
|
+
} from '../redux/api';
|
|
13
|
+
|
|
14
|
+
import amex from '../../assets/img/amex.jpg';
|
|
15
|
+
import mastercard from '../../assets/img/mastercard.png';
|
|
16
|
+
import other from '../../assets/img/other.png';
|
|
17
|
+
import troy from '../../assets/img/troy.png';
|
|
18
|
+
import visa from '../../assets/img/visa.png';
|
|
19
|
+
import { setCards } from '../redux/reducer';
|
|
20
|
+
import { DeleteConfirmationModal } from '../components/delete-confirmation-modal';
|
|
21
|
+
import {
|
|
22
|
+
AgreementAndSubmitProps,
|
|
23
|
+
CardSelectionSectionProps,
|
|
24
|
+
ErrorTexts,
|
|
25
|
+
InstallmentSectionProps,
|
|
26
|
+
SavedCardOptionTexts
|
|
27
|
+
} from '../types';
|
|
28
|
+
import { CardSelectionSection } from '../components/card-selection-section';
|
|
29
|
+
import { InstallmentSection } from '../components/installment-section';
|
|
30
|
+
import { AgreementAndSubmit } from '../components/agreement-and-submit';
|
|
31
|
+
|
|
32
|
+
export const cardImages = { amex, mastercard, troy, visa, other };
|
|
33
|
+
|
|
34
|
+
type SavedCardOptionProps = {
|
|
35
|
+
texts?: SavedCardOptionTexts;
|
|
36
|
+
agreementCheckbox?: ReactElement;
|
|
37
|
+
customRender?: {
|
|
38
|
+
cardSelectionSection?: (props: CardSelectionSectionProps) => ReactElement;
|
|
39
|
+
installmentSection?: (props: InstallmentSectionProps) => ReactElement;
|
|
40
|
+
agreementAndSubmit?: (props: AgreementAndSubmitProps) => ReactElement;
|
|
41
|
+
};
|
|
42
|
+
formWrapperClassName?: string;
|
|
43
|
+
cardSelectionWrapperClassName?: string;
|
|
44
|
+
installmentWrapperClassName?: string;
|
|
45
|
+
formProps?: React.FormHTMLAttributes<HTMLFormElement>;
|
|
46
|
+
cardSelectionWrapperProps?: React.HTMLAttributes<HTMLDivElement>;
|
|
47
|
+
installmentWrapperProps?: React.HTMLAttributes<HTMLDivElement>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const defaultTranslations: SavedCardOptionTexts = {
|
|
51
|
+
title: 'Pay with Saved Card',
|
|
52
|
+
button: 'Continue with selected card',
|
|
53
|
+
installment: {
|
|
54
|
+
title: 'Installment Options'
|
|
55
|
+
},
|
|
56
|
+
errors: {
|
|
57
|
+
required: 'This field is required'
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const mergeTranslations = (
|
|
62
|
+
customTranslations: SavedCardOptionTexts,
|
|
63
|
+
defaultTranslations: SavedCardOptionTexts
|
|
64
|
+
) => {
|
|
65
|
+
return {
|
|
66
|
+
...defaultTranslations,
|
|
67
|
+
...customTranslations,
|
|
68
|
+
installment: {
|
|
69
|
+
...defaultTranslations.installment,
|
|
70
|
+
...customTranslations.installment
|
|
71
|
+
},
|
|
72
|
+
errors: {
|
|
73
|
+
...defaultTranslations.errors,
|
|
74
|
+
...customTranslations.errors
|
|
75
|
+
},
|
|
76
|
+
deletePopup: {
|
|
77
|
+
...defaultTranslations.deletePopup,
|
|
78
|
+
...customTranslations.deletePopup
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const createFormSchema = (errors: ErrorTexts) =>
|
|
84
|
+
yup.object().shape({
|
|
85
|
+
card: yup
|
|
86
|
+
.string()
|
|
87
|
+
.required(errors?.required ?? defaultTranslations.errors.required)
|
|
88
|
+
.typeError(errors?.required ?? defaultTranslations.errors.required),
|
|
89
|
+
agreement: yup
|
|
90
|
+
.boolean()
|
|
91
|
+
.oneOf([true], errors?.required ?? defaultTranslations.errors.required)
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const SavedCardOption = ({
|
|
95
|
+
texts = defaultTranslations,
|
|
96
|
+
agreementCheckbox,
|
|
97
|
+
customRender,
|
|
98
|
+
formWrapperClassName = 'flex flex-wrap w-full',
|
|
99
|
+
cardSelectionWrapperClassName = 'w-full flex flex-col xl:w-6/10',
|
|
100
|
+
installmentWrapperClassName = 'w-full xl:w-4/10 xl:border-l xl:border-t-0',
|
|
101
|
+
formProps = {},
|
|
102
|
+
cardSelectionWrapperProps = {},
|
|
103
|
+
installmentWrapperProps = {}
|
|
104
|
+
}: SavedCardOptionProps) => {
|
|
105
|
+
const mergedTexts = useMemo(
|
|
106
|
+
() => mergeTranslations(texts, defaultTranslations),
|
|
107
|
+
[texts]
|
|
108
|
+
);
|
|
109
|
+
const dispatch = useAppDispatch();
|
|
110
|
+
const { data: savedCards } = useGetSavedCardsQuery();
|
|
111
|
+
const [completeSavedCard] = useCompleteSavedCardMutation();
|
|
112
|
+
const [setSavedCard] = useSetSavedCardMutation();
|
|
113
|
+
const installmentOptions = useAppSelector(
|
|
114
|
+
(state) => state.checkout.installmentOptions
|
|
115
|
+
);
|
|
116
|
+
const cards = useAppSelector((state) => state.savedCard.cards);
|
|
117
|
+
|
|
118
|
+
const {
|
|
119
|
+
register,
|
|
120
|
+
handleSubmit,
|
|
121
|
+
control,
|
|
122
|
+
formState: { errors },
|
|
123
|
+
setValue,
|
|
124
|
+
watch
|
|
125
|
+
} = useForm({
|
|
126
|
+
resolver: yupResolver(createFormSchema(mergedTexts.errors))
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const selectedCardToken = watch('card');
|
|
130
|
+
const selectedCard = useMemo(
|
|
131
|
+
() => cards?.find((card) => card.token === selectedCardToken),
|
|
132
|
+
[cards, selectedCardToken]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const handleCardSelection = async (card) => {
|
|
136
|
+
await setSavedCard({ card }).unwrap();
|
|
137
|
+
setValue('card', card.token, { shouldValidate: true });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const onSubmit = async () => {
|
|
141
|
+
try {
|
|
142
|
+
await completeSavedCard({ agreement: true });
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Error completing saved card:', error);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (savedCards?.results && !cards?.length) {
|
|
150
|
+
dispatch(setCards(savedCards.results));
|
|
151
|
+
}
|
|
152
|
+
}, [savedCards, cards, dispatch]);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<>
|
|
156
|
+
<form
|
|
157
|
+
className={formWrapperClassName}
|
|
158
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
159
|
+
{...formProps}
|
|
160
|
+
>
|
|
161
|
+
<div
|
|
162
|
+
className={cardSelectionWrapperClassName}
|
|
163
|
+
{...cardSelectionWrapperProps}
|
|
164
|
+
>
|
|
165
|
+
{customRender?.cardSelectionSection ? (
|
|
166
|
+
customRender.cardSelectionSection({
|
|
167
|
+
title: mergedTexts.title,
|
|
168
|
+
cards,
|
|
169
|
+
selectedCard,
|
|
170
|
+
onSelect: handleCardSelection,
|
|
171
|
+
register,
|
|
172
|
+
errors,
|
|
173
|
+
dispatch
|
|
174
|
+
})
|
|
175
|
+
) : (
|
|
176
|
+
<CardSelectionSection
|
|
177
|
+
title={mergedTexts.title}
|
|
178
|
+
cards={cards}
|
|
179
|
+
selectedCard={selectedCard}
|
|
180
|
+
onSelect={handleCardSelection}
|
|
181
|
+
register={register}
|
|
182
|
+
errors={errors}
|
|
183
|
+
dispatch={dispatch}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div
|
|
189
|
+
className={installmentWrapperClassName}
|
|
190
|
+
{...installmentWrapperProps}
|
|
191
|
+
>
|
|
192
|
+
{customRender?.installmentSection ? (
|
|
193
|
+
customRender.installmentSection({
|
|
194
|
+
title: mergedTexts.installment?.title,
|
|
195
|
+
selectedCard,
|
|
196
|
+
installmentOptions,
|
|
197
|
+
translations: mergedTexts.installment,
|
|
198
|
+
errors: errors.installment
|
|
199
|
+
})
|
|
200
|
+
) : (
|
|
201
|
+
<InstallmentSection
|
|
202
|
+
title={mergedTexts.installment?.title}
|
|
203
|
+
selectedCard={selectedCard}
|
|
204
|
+
installmentOptions={installmentOptions}
|
|
205
|
+
translations={mergedTexts.installment}
|
|
206
|
+
errors={errors.installment}
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{customRender?.agreementAndSubmit ? (
|
|
211
|
+
customRender.agreementAndSubmit({
|
|
212
|
+
agreementCheckbox,
|
|
213
|
+
control,
|
|
214
|
+
errors,
|
|
215
|
+
buttonText: mergedTexts.button
|
|
216
|
+
})
|
|
217
|
+
) : (
|
|
218
|
+
<AgreementAndSubmit
|
|
219
|
+
agreementCheckbox={agreementCheckbox}
|
|
220
|
+
control={control}
|
|
221
|
+
errors={errors}
|
|
222
|
+
buttonText={mergedTexts.button}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
</form>
|
|
227
|
+
<DeleteConfirmationModal translations={mergedTexts.deletePopup} />
|
|
228
|
+
</>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export default SavedCardOption;
|
package/src/redux/middleware.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Middleware } from '@reduxjs/toolkit';
|
|
2
|
-
import { setUcs } from './reducer';
|
|
3
|
-
|
|
4
|
-
const savedCardMiddleware: Middleware = ({ dispatch }) => {
|
|
5
|
-
return (next) => (action) => {
|
|
6
|
-
const result = next(action);
|
|
7
|
-
|
|
8
|
-
const ucsContext = result.payload?.context_list?.find(
|
|
9
|
-
(context) => context.page_context.ucs
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
if (ucsContext) {
|
|
13
|
-
const ucs = JSON.parse(JSON.stringify(ucsContext.page_context.ucs));
|
|
14
|
-
|
|
15
|
-
const match = ucs.script.match(/<script\b[^>]*>([\s\S]*?)<\/script>/);
|
|
16
|
-
ucs.script = match ? match[1] : ucs.script;
|
|
17
|
-
|
|
18
|
-
dispatch(setUcs(ucs));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return result;
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export default savedCardMiddleware;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { cookies } from 'next/headers';
|
|
2
|
-
import settings from 'settings';
|
|
3
|
-
import { redirect } from 'next/navigation';
|
|
4
|
-
import { ROUTES } from 'routes';
|
|
5
|
-
|
|
6
|
-
export const SavedCardRedirect = async () => {
|
|
7
|
-
const nextCookies = cookies();
|
|
8
|
-
const sessionId = nextCookies.get('osessionid')?.value;
|
|
9
|
-
|
|
10
|
-
if (!sessionId) {
|
|
11
|
-
return redirect(ROUTES.CHECKOUT);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const commerceUrl = settings.commerceUrl;
|
|
15
|
-
|
|
16
|
-
const response = await fetch(`${commerceUrl}/orders/saved-card-redirect`, {
|
|
17
|
-
headers: {
|
|
18
|
-
Cookie: nextCookies.toString()
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const data = await response.text();
|
|
23
|
-
|
|
24
|
-
return <div dangerouslySetInnerHTML={{ __html: data }}></div>;
|
|
25
|
-
};
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { Price, Radio } from '@akinon/next/components';
|
|
2
|
-
import { useLocalization } from '@akinon/next/hooks';
|
|
3
|
-
import { useSetInstallmentMutation } from '../endpoints';
|
|
4
|
-
import { useEffect, useState } from 'react';
|
|
5
|
-
|
|
6
|
-
const defaultTranslations = {
|
|
7
|
-
payments: 'Payments',
|
|
8
|
-
per_month: 'Per Month',
|
|
9
|
-
total: 'Total'
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const SavedCardInstallments = ({ installmentOptions, translations, error }) => {
|
|
13
|
-
const { t } = useLocalization();
|
|
14
|
-
const [installmentOption, setInstallmentOption] = useState(null);
|
|
15
|
-
const [setInstallment] = useSetInstallmentMutation();
|
|
16
|
-
|
|
17
|
-
const errorMessage = (
|
|
18
|
-
<div className="px-6 mt-4 text-sm text-error">{error?.message}</div>
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (
|
|
23
|
-
installmentOptions[0]?.pk &&
|
|
24
|
-
installmentOption !== installmentOptions[0]?.pk
|
|
25
|
-
) {
|
|
26
|
-
setInstallment(installmentOptions[0].pk);
|
|
27
|
-
setInstallmentOption(installmentOptions[0].pk);
|
|
28
|
-
}
|
|
29
|
-
}, [installmentOptions, setInstallment]);
|
|
30
|
-
|
|
31
|
-
if (installmentOptions.length === 0) {
|
|
32
|
-
return (
|
|
33
|
-
<>
|
|
34
|
-
<div className="text-xs text-black-800 p-4 sm:p-6">
|
|
35
|
-
{t('checkout.payment.installment_options.description')}
|
|
36
|
-
</div>
|
|
37
|
-
</>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<>
|
|
43
|
-
<div>
|
|
44
|
-
<div className="px-4 mb-4 sm:px-6 sm:mb-6">
|
|
45
|
-
<table className="w-full border-t border-b border-solid border-gray-400">
|
|
46
|
-
<thead>
|
|
47
|
-
<tr>
|
|
48
|
-
<th
|
|
49
|
-
scope="col"
|
|
50
|
-
className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 border-0 text-start"
|
|
51
|
-
>
|
|
52
|
-
{translations?.payments ?? defaultTranslations.payments}
|
|
53
|
-
</th>
|
|
54
|
-
<th
|
|
55
|
-
scope="col"
|
|
56
|
-
className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 border-0 text-right"
|
|
57
|
-
>
|
|
58
|
-
{translations?.per_month ?? defaultTranslations.per_month}
|
|
59
|
-
</th>
|
|
60
|
-
<th
|
|
61
|
-
scope="col"
|
|
62
|
-
className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 border-0 text-right"
|
|
63
|
-
>
|
|
64
|
-
{translations?.total ?? defaultTranslations.total}
|
|
65
|
-
</th>
|
|
66
|
-
</tr>
|
|
67
|
-
</thead>
|
|
68
|
-
<tbody>
|
|
69
|
-
{installmentOptions.map((option) => (
|
|
70
|
-
<tr
|
|
71
|
-
key={`installment-${option.pk}`}
|
|
72
|
-
className="border-t border-solid border-gray-400"
|
|
73
|
-
>
|
|
74
|
-
<td className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 text-left">
|
|
75
|
-
<Radio
|
|
76
|
-
value={option.pk}
|
|
77
|
-
name="installment"
|
|
78
|
-
checked={option.pk === installmentOption}
|
|
79
|
-
onChange={() => {
|
|
80
|
-
setInstallmentOption(option.pk);
|
|
81
|
-
setInstallment(option.pk);
|
|
82
|
-
}}
|
|
83
|
-
>
|
|
84
|
-
<span className="w-full flex items-center justify-start pl-2">
|
|
85
|
-
<span className="text-xs text-black-800 transition-all">
|
|
86
|
-
{option.label}
|
|
87
|
-
</span>
|
|
88
|
-
</span>
|
|
89
|
-
</Radio>
|
|
90
|
-
</td>
|
|
91
|
-
<td className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 text-right">
|
|
92
|
-
<Price value={option.monthly_price_with_accrued_interest} />
|
|
93
|
-
</td>
|
|
94
|
-
<td className="text-xs font-normal border-e border-solid border-gray-400 px-2 py-2 text-right">
|
|
95
|
-
<Price value={option.price_with_accrued_interest} />
|
|
96
|
-
</td>
|
|
97
|
-
</tr>
|
|
98
|
-
))}
|
|
99
|
-
</tbody>
|
|
100
|
-
</table>
|
|
101
|
-
</div>
|
|
102
|
-
{error && errorMessage}
|
|
103
|
-
</div>
|
|
104
|
-
</>
|
|
105
|
-
);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
export default SavedCardInstallments;
|