@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.
@@ -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;