@akinon/pz-saved-card 1.67.0 → 1.69.0-rc.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,17 +1,51 @@
1
- export {};
1
+ import { ReactElement } from 'react';
2
2
 
3
- declare global {
4
- interface Window {
5
- iyziUcsInit: {
6
- scriptType: string;
7
- ucsToken: string;
8
- createTag: () => void;
9
- };
10
- universalCardStorage: {
11
- binNumber: number;
12
- cardToken: string;
13
- consumerToken: string;
14
- registerConsumerCard: boolean;
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;
@@ -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;