@akinon/pz-saved-card 1.62.0-rc.24 → 1.63.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,400 @@
1
+ 'use client';
2
+
3
+ import * as yup from 'yup';
4
+ import { cloneElement, ReactElement, useEffect, useState } from 'react';
5
+ import { useForm } from 'react-hook-form';
6
+ import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
7
+ import {
8
+ useSetCompleteSavedCardMutation,
9
+ useSetSavedCardMutation
10
+ } from '../endpoints';
11
+ import { yupResolver } from '@hookform/resolvers/yup';
12
+ import Script from 'next/script';
13
+ import { resetUcs } from '../redux/reducer';
14
+ import { Button, Icon, Image, Input, Select } from '@akinon/next/components';
15
+ import SavedCardInstallments from './installments';
16
+ import '../types';
17
+
18
+ type SavedCardOptionProps = {
19
+ texts?: {
20
+ title?: string;
21
+ button?: string;
22
+ installment?: {
23
+ title?: string;
24
+ payments?: string;
25
+ };
26
+ label?: {
27
+ cardNumber?: string;
28
+ expiryDate?: string;
29
+ cvv?: string;
30
+ };
31
+ placeholder?: {
32
+ cardNumber?: string;
33
+ cardMonth?: string;
34
+ cardYear?: string;
35
+ cardCvv?: string;
36
+ };
37
+ errors?: {
38
+ required?: string;
39
+ cardNumberLength?: string;
40
+ cvvLength?: string;
41
+ saveCard?: string;
42
+ };
43
+ };
44
+ agreementCheckbox?: ReactElement;
45
+ };
46
+
47
+ const defaultTranslations: SavedCardOptionProps = {
48
+ texts: {
49
+ title: 'Pay with Iyzico Saved Card',
50
+ button: 'Continue with selected card',
51
+ installment: {
52
+ title: 'Installment Options'
53
+ },
54
+ label: {
55
+ cardNumber: 'Card Number',
56
+ expiryDate: 'Expiration Date',
57
+ cvv: 'CVC'
58
+ },
59
+ placeholder: {
60
+ cardMonth: 'Month',
61
+ cardYear: 'year'
62
+ },
63
+ errors: {
64
+ required: 'This field is required',
65
+ cardNumberLength: 'Card number must be 16 digits long.',
66
+ cvvLength: 'CVV must be 3 digits long.',
67
+ saveCard:
68
+ "Please check the 'Save my card with Iyzico' option to proceed with the payment."
69
+ }
70
+ }
71
+ };
72
+
73
+ const formSchema = (translations) => {
74
+ return yup.object().shape({
75
+ card_number: yup
76
+ .string()
77
+ .transform((value: string) => value.replace(/_/g, '').replace(/ /g, ''))
78
+ .length(
79
+ 16,
80
+ translations?.cardNumberLength ??
81
+ defaultTranslations.texts.errors.cardNumberLength
82
+ )
83
+ .required(
84
+ translations?.required ?? defaultTranslations.texts.errors.required
85
+ ),
86
+ card_month: yup
87
+ .string()
88
+ .required(
89
+ translations?.required ?? defaultTranslations.texts.errors.required
90
+ ),
91
+ card_year: yup
92
+ .string()
93
+ .required(
94
+ translations?.required ?? defaultTranslations.texts.errors.required
95
+ ),
96
+ card_cvv: yup
97
+ .string()
98
+ .transform((value: string) => value.replace(/_/g, '').replace(/ /g, ''))
99
+ .length(
100
+ 3,
101
+ translations?.cvvLength ?? defaultTranslations.texts.errors.cvvLength
102
+ )
103
+ .required(
104
+ translations?.required ?? defaultTranslations.texts.errors.required
105
+ ),
106
+ agreement: yup
107
+ .boolean()
108
+ .oneOf(
109
+ [true],
110
+ translations?.required ?? defaultTranslations.texts.errors.required
111
+ )
112
+ });
113
+ };
114
+
115
+ export function Option(props: SavedCardOptionProps) {
116
+ const dispatch = useAppDispatch();
117
+ const [formError, setFormError] = useState(null);
118
+ const [months, setMonths] = useState([]);
119
+ const [years, setYears] = useState([]);
120
+ const [installmentOptions, setInstallmentOptions] = useState([]);
121
+
122
+ const ucs = useAppSelector((state) => state.saved_card.ucs);
123
+ const [setSavedCard] = useSetSavedCardMutation();
124
+ const [setCompleteSavedCard] = useSetCompleteSavedCardMutation();
125
+
126
+ const {
127
+ register,
128
+ handleSubmit,
129
+ control,
130
+ formState: { errors }
131
+ } = useForm({
132
+ resolver: yupResolver(formSchema(props.texts?.errors))
133
+ });
134
+
135
+ useEffect(() => {
136
+ window.iyziUcsInit?.createTag();
137
+
138
+ const months = [
139
+ {
140
+ label:
141
+ props.texts?.placeholder?.cardMonth ??
142
+ defaultTranslations.texts.placeholder.cardMonth,
143
+ value: ''
144
+ }
145
+ ];
146
+ const years = [
147
+ {
148
+ label:
149
+ props.texts?.placeholder?.cardYear ??
150
+ defaultTranslations.texts.placeholder.cardYear,
151
+ value: ''
152
+ }
153
+ ];
154
+ const date = new Date();
155
+ const currentYear = date.getFullYear();
156
+
157
+ for (let i = 1; i <= 12; i++) {
158
+ months.push({ label: i.toString(), value: i.toString() });
159
+ }
160
+
161
+ for (let i = currentYear; i < currentYear + 13; i++) {
162
+ years.push({ label: i.toString(), value: i.toString() });
163
+ }
164
+
165
+ setMonths(months);
166
+ setYears(years);
167
+ setSavedCard(window.iyziUcsInit.ucsToken).then((res) => {
168
+ if (!('data' in res)) {
169
+ return;
170
+ }
171
+
172
+ const installmentContext = res.data?.context_list?.find(
173
+ (context) => context.page_name === 'SavedCardInstallmentSelectionPage'
174
+ );
175
+
176
+ if (installmentContext) {
177
+ setInstallmentOptions(installmentContext.page_context.installments);
178
+ }
179
+ });
180
+
181
+ return () => {
182
+ dispatch(resetUcs());
183
+ };
184
+ }, []);
185
+
186
+ const onSubmit = async (data) => {
187
+ if (!window.iyziUcsInit) {
188
+ return;
189
+ }
190
+
191
+ setFormError({});
192
+
193
+ if (window.iyziUcsInit.scriptType === 'CONSUMER_WITH_CARD_EXIST') {
194
+ const res = await setCompleteSavedCard({
195
+ ucs_token: window.iyziUcsInit.ucsToken,
196
+ consumer_token: window.universalCardStorage.consumerToken,
197
+ card_token: window.universalCardStorage.cardToken
198
+ });
199
+
200
+ if ('data' in res && res.data.errors) {
201
+ setFormError(res.data.errors);
202
+ }
203
+ return;
204
+ } else {
205
+ if (!window.universalCardStorage.registerConsumerCard) {
206
+ setFormError({
207
+ non_field_errors:
208
+ props.texts?.errors?.saveCard ??
209
+ defaultTranslations.texts.errors.saveCard
210
+ });
211
+ return;
212
+ }
213
+ const res = await setCompleteSavedCard({
214
+ agreement: data.agreement,
215
+ card_number: data.card_number.replaceAll(' ', ''),
216
+ card_month: data.card_month,
217
+ card_year: data.card_year,
218
+ card_cvv: data.card_cvv,
219
+ register_consumer_card: window.universalCardStorage.registerConsumerCard
220
+ });
221
+
222
+ if ('data' in res && res.data.errors) {
223
+ setFormError(res.data.errors);
224
+ }
225
+ }
226
+ };
227
+
228
+ const ucsScript = ucs.script && (
229
+ <Script
230
+ id="saved_card"
231
+ strategy="afterInteractive"
232
+ dangerouslySetInnerHTML={{ __html: ucs.script }}
233
+ />
234
+ );
235
+
236
+ return (
237
+ <>
238
+ <form
239
+ className="flex flex-row flex-wrap w-full"
240
+ onSubmit={handleSubmit(onSubmit)}
241
+ >
242
+ <div className="w-full flex flex-col xl:w-6/10">
243
+ <div className="flex justify-start items-center border-solid border-gray-400 px-4 py-2 sm:px-6 sm:py-3 sm:min-h-15">
244
+ <span className="text-black-800 text-lg font-medium sm:text-2xl">
245
+ {props.texts?.title ?? defaultTranslations.texts.title}
246
+ </span>
247
+ </div>
248
+
249
+ {window.iyziUcsInit?.scriptType === 'UCS_CONSENT' && (
250
+ <>
251
+ <div className="w-full bg-white">
252
+ <div className="px-4 my-2 w-full flex justify-between flex-wrap pb-4 sm:px-6 sm:py-6">
253
+ <div className="my-2 w-full flex flex-col sm:px-4">
254
+ <div className="text-xs text-gray-800 mb-2 w-full flex justify-between items-center">
255
+ <span>
256
+ {props.texts?.label?.cardNumber ??
257
+ defaultTranslations.texts.label.cardNumber}
258
+ </span>
259
+ </div>
260
+
261
+ <Input
262
+ format="#### #### #### ####"
263
+ mask="_"
264
+ allowEmptyFormatting={true}
265
+ control={control}
266
+ {...register('card_number')}
267
+ error={errors.card_number}
268
+ />
269
+ </div>
270
+
271
+ <div className="w-full my-2 sm:flex">
272
+ <div className="sm:w-2/3 sm:px-4">
273
+ <label
274
+ className="flex w-full text-xs text-start text-black-400 mb-1.5"
275
+ htmlFor="card_month"
276
+ >
277
+ {props.texts?.label?.expiryDate ??
278
+ defaultTranslations.texts.label.expiryDate}
279
+ </label>
280
+
281
+ <div className="flex w-full h-10 space-x-2.5">
282
+ <div className="w-2/4">
283
+ <Select
284
+ className="w-full text-xs border-gray-400 sm:text-sm"
285
+ options={months}
286
+ {...register('card_month')}
287
+ error={errors.card_month}
288
+ />
289
+ </div>
290
+
291
+ <div className="w-2/4">
292
+ <Select
293
+ className="w-full text-xs border-gray-400 sm:text-sm"
294
+ options={years}
295
+ {...register('card_year')}
296
+ error={errors.card_year}
297
+ />
298
+ </div>
299
+ </div>
300
+ </div>
301
+ <div className="my-2 sm:w-1/3 sm:px-4 sm:my-0">
302
+ <label
303
+ className="flex w-full text-xs text-start text-black-400 mb-1.5"
304
+ htmlFor="card_cvv"
305
+ >
306
+ {props.texts?.label?.cvv ??
307
+ defaultTranslations.texts.label.cvv}
308
+ </label>
309
+ <Input
310
+ format="###"
311
+ mask="_"
312
+ control={control}
313
+ allowEmptyFormatting={true}
314
+ {...register('card_cvv')}
315
+ error={errors.card_cvv}
316
+ />
317
+ <div className="group relative flex items-center justify-start text-gray-600 cursor-pointer mt-2 transition-all hover:text-secondary">
318
+ <span className="text-xs underline">
319
+ {props.texts?.label?.cvv ??
320
+ defaultTranslations.texts.label.cvv}
321
+ </span>
322
+ <Icon
323
+ name="cvc"
324
+ size={16}
325
+ className="leading-none ml-2"
326
+ />
327
+ <div className="hidden group-hover:block absolute right-0 bottom-5 w-[11rem] lg:w-[21rem] lg:left-auto lg:right-auto border-2">
328
+ <Image
329
+ src="/cvv.jpg"
330
+ alt="Cvv"
331
+ width={385}
332
+ height={262}
333
+ />
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+ <div id="ucs-permission"></div>
341
+ </>
342
+ )}
343
+ {window.iyziUcsInit?.scriptType !== 'UCS_CONSENT' && (
344
+ <div id="ucs-cards"></div>
345
+ )}
346
+ </div>
347
+
348
+ <div className="w-full flex flex-col border-gray-400 border-solid xl:w-4/10 xl:border-l xl:border-t-0">
349
+ <div className="flex justify-start items-center border-solid border-gray-400 px-4 py-2 sm:px-6 sm:py-3 sm:min-h-15">
350
+ <span className="text-black-800 text-lg font-medium sm:text-2xl">
351
+ {props.texts?.installment?.title ??
352
+ defaultTranslations.texts.installment.title}
353
+ </span>
354
+ </div>
355
+
356
+ <div className="w-full border-solid border-gray-400 bg-white">
357
+ <SavedCardInstallments
358
+ installmentOptions={installmentOptions}
359
+ translations={props.texts?.installment}
360
+ error={errors.installment}
361
+ />
362
+
363
+ <div className="flex flex-col text-xs pb-4 px-4 sm:px-6 sm:pb-6 md:mt-0 mt-4">
364
+ {props.agreementCheckbox &&
365
+ cloneElement(props.agreementCheckbox, {
366
+ control,
367
+ error: errors.agreement,
368
+ fieldId: 'agreement'
369
+ })}
370
+ {formError?.non_field_errors && (
371
+ <div className="w-full text-xs text-start px-1 mt-3 text-error">
372
+ {formError.non_field_errors}
373
+ </div>
374
+ )}
375
+ {formError?.status && (
376
+ <div className="w-full text-xs text-start px-1 mt-3 text-error">
377
+ {formError.status}
378
+ </div>
379
+ )}
380
+ <Button
381
+ className="group uppercase mt-4 inline-flex items-center justify-center"
382
+ type="submit"
383
+ >
384
+ <span>
385
+ {props.texts?.button ?? defaultTranslations.texts.button}
386
+ </span>
387
+ <Icon
388
+ name="chevron-end"
389
+ size={12}
390
+ className="fill-primary-foreground ml-2 h-3 group-hover:fill-primary"
391
+ />
392
+ </Button>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ </form>
397
+ {ucsScript}
398
+ </>
399
+ );
400
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,25 +0,0 @@
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
- );
@@ -1,17 +0,0 @@
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
- );
@@ -1,51 +0,0 @@
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
- );
@@ -1,80 +0,0 @@
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
- };
@@ -1,8 +0,0 @@
1
- import { Icon } from '@akinon/next/components';
2
- import React from 'react';
3
-
4
- export const DeleteIcon = ({ onClick }) => (
5
- <span className="cursor-pointer p-1" onClick={onClick}>
6
- <Icon name="close" size={12} />
7
- </span>
8
- );
@@ -1,7 +0,0 @@
1
- import React from 'react';
2
-
3
- export const ErrorText = ({ message }) => (
4
- <div className="w-full text-xs text-start px-1 mt-3 text-error">
5
- {message}
6
- </div>
7
- );
@@ -1,22 +0,0 @@
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
- );