@akinon/pz-saved-card 1.96.0-snapshot-ZERO-3620-20250915165755 → 1.97.0-snapshot-ZERO-3634-20250918132143

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 CHANGED
@@ -1,50 +1,12 @@
1
1
  # @akinon/pz-saved-card
2
2
 
3
- ## 1.96.0-snapshot-ZERO-3620-20250915165755
3
+ ## 1.97.0-snapshot-ZERO-3634-20250918132143
4
4
 
5
5
  ### Minor Changes
6
6
 
7
- - d8be48fb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
8
- - 16aff543: ZERO-3431: Add test script for redirect utility in package.json
9
- - e974d8e8: ZERO-3406: Fix rc build
10
- - 7eb51ca9: ZERO-3424 :Update package versions
11
- - 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
12
- - 8b1d24eb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
13
- - 33377cfd: ZERO-3267: Refactor import statement for ROUTES in error-page component
7
+ - 5b50079: ZERO-3634: iyzico saved card
14
8
 
15
- ## 1.96.0-rc.61
16
-
17
- ### Minor Changes
18
-
19
- - d8be48fb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
20
- - 16aff543: ZERO-3431: Add test script for redirect utility in package.json
21
- - e974d8e8: ZERO-3406: Fix rc build
22
- - 7eb51ca9: ZERO-3424 :Update package versions
23
- - 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
24
- - 8b1d24eb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
25
- - 33377cfd: ZERO-3267: Refactor import statement for ROUTES in error-page component
26
-
27
- ## 1.96.0-rc.60
28
-
29
- ## 1.96.0-rc.59
30
-
31
- ## 1.96.0-rc.58
32
-
33
- ## 1.96.0-rc.57
34
-
35
- ## 1.96.0-rc.56
36
-
37
- ## 1.96.0-rc.55
38
-
39
- ### Minor Changes
40
-
41
- - d8be48fb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
42
- - 16aff543: ZERO-3431: Add test script for redirect utility in package.json
43
- - e974d8e8: ZERO-3406: Fix rc build
44
- - 7eb51ca9: ZERO-3424 :Update package versions
45
- - 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
46
- - 8b1d24eb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
47
- - 33377cfd: ZERO-3267: Refactor import statement for ROUTES in error-page component
9
+ ## 1.96.0
48
10
 
49
11
  ## 1.95.0
50
12
 
@@ -56,83 +18,14 @@
56
18
 
57
19
  ## 1.91.0
58
20
 
59
- > > > > > > > origin/ZERO-3418
60
-
61
21
  ## 1.90.0
62
22
 
63
23
  ## 1.89.0
64
24
 
65
25
  ### Minor Changes
66
26
 
67
- - e974d8e: ZERO-3406: Fix rc build
68
- - 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
69
- - 33377cfd: ZERO-3267: Refactor import statement for ROUTES in error-page component
70
-
71
- ## 1.89.0-rc.21
72
-
73
- ### Minor Changes
74
-
75
- - e974d8e: ZERO-3406: Fix rc build
76
-
77
- ## 1.89.0-rc.20
78
-
79
- ## 1.89.0-rc.19
80
-
81
- ## 1.89.0-rc.18
82
-
83
- ## 1.89.0-rc.17
84
-
85
- ## 1.89.0-rc.16
86
-
87
- ## 1.89.0-rc.15
88
-
89
- ## 1.89.0-rc.14
90
-
91
- ## 1.89.0-rc.13
92
-
93
- ## 1.89.0-rc.12
94
-
95
- ## 1.89.0-rc.11
96
-
97
- ## 1.89.0-rc.10
98
-
99
- ### Minor Changes
100
-
101
- - 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
102
- - 33377cfd: ZERO-3267: Refactor import statement for ROUTES in error-page component
103
- - e2026ec: ZERO-3353: add missing test ids for e2e test
104
-
105
- ## 1.89.0-rc.9
106
-
107
- ### Minor Changes
108
-
109
- - 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
110
- - 33377cfd: ZERO-3267: Refactor import statement for ROUTES in error-page component
111
27
  - e2026ec: ZERO-3353: add missing test ids for e2e test
112
28
 
113
- ## 1.89.0-rc.8
114
-
115
- ## 1.89.0-rc.7
116
-
117
- ## 1.89.0-rc.6
118
-
119
- ## 1.89.0-rc.5
120
-
121
- ## 1.89.0-rc.4
122
-
123
- ## 1.89.0-rc.3
124
-
125
- ## 1.89.0-rc.2
126
-
127
- ## 1.89.0-rc.1
128
-
129
- ## 1.89.0-rc.0
130
-
131
- ### Minor Changes
132
-
133
- - 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
134
- - 33377cf: ZERO-3267: Refactor import statement for ROUTES in error-page component
135
-
136
29
  ## 1.88.0
137
30
 
138
31
  ## 1.87.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/pz-saved-card",
3
- "version": "1.96.0-snapshot-ZERO-3620-20250915165755",
3
+ "version": "1.97.0-snapshot-ZERO-3634-20250918132143",
4
4
  "license": "MIT",
5
5
  "main": "src/index.tsx",
6
6
  "peerDependencies": {
@@ -1,12 +1,14 @@
1
- import React, { cloneElement } from 'react';
1
+ import { cloneElement } from 'react';
2
2
  import { Button, Icon } from '@akinon/next/components';
3
+ import { AgreementAndSubmitProps } from '../types';
3
4
 
4
5
  export const AgreementAndSubmit = ({
5
6
  agreementCheckbox,
6
7
  control,
7
8
  errors,
8
- buttonText
9
- }) => (
9
+ buttonText,
10
+ formError
11
+ }: AgreementAndSubmitProps) => (
10
12
  <div className="flex flex-col text-xs pb-4 px-4 sm:px-6">
11
13
  {agreementCheckbox &&
12
14
  cloneElement(agreementCheckbox, {
@@ -14,6 +16,16 @@ export const AgreementAndSubmit = ({
14
16
  error: errors.agreement,
15
17
  fieldId: 'agreement'
16
18
  })}
19
+ {formError?.non_field_errors && (
20
+ <div className="w-full text-xs text-start px-1 mt-3 text-error">
21
+ {formError.non_field_errors}
22
+ </div>
23
+ )}
24
+ {formError?.status && (
25
+ <div className="w-full text-xs text-start px-1 mt-3 text-error">
26
+ {formError.status}
27
+ </div>
28
+ )}
17
29
  <Button
18
30
  type="submit"
19
31
  className="group uppercase mt-4 inline-flex items-center justify-center"
@@ -0,0 +1,125 @@
1
+ import React from 'react';
2
+ import { Input, Select, Icon, Image } from '@akinon/next/components';
3
+ import { Control, UseFormRegister, FieldErrors } from 'react-hook-form';
4
+
5
+ export interface CardFormSectionProps {
6
+ register: UseFormRegister<any>;
7
+ control: Control<any>;
8
+ errors: FieldErrors;
9
+ months: Array<{ label: string; value: string }>;
10
+ years: Array<{ label: string; value: string }>;
11
+ translations?: {
12
+ cardHolder?: string;
13
+ cardNumber?: string;
14
+ expiryDate?: string;
15
+ cvv?: string;
16
+ cvvTooltip?: string;
17
+ };
18
+ }
19
+
20
+ export const CardFormSection = ({
21
+ register,
22
+ control,
23
+ errors,
24
+ months,
25
+ years,
26
+ translations = {}
27
+ }: CardFormSectionProps) => {
28
+ const defaultTranslations = {
29
+ cardHolder: 'Cardholder Name',
30
+ cardNumber: 'Card Number',
31
+ expiryDate: 'Expiration Date',
32
+ cvv: 'CVV',
33
+ cvvTooltip: 'CVV'
34
+ };
35
+
36
+ const mergedTranslations = { ...defaultTranslations, ...translations };
37
+
38
+ return (
39
+ <div className="w-full bg-white">
40
+ <div className="px-4 my-2 w-full flex justify-between flex-wrap">
41
+ <div className="my-2 w-full sm:px-4">
42
+ <Input
43
+ label={mergedTranslations.cardHolder}
44
+ {...register('card_holder')}
45
+ error={errors.card_holder}
46
+ />
47
+ </div>
48
+
49
+ <div className="my-2 w-full flex flex-col sm:px-4">
50
+ <div className="text-xs text-gray-800 mb-2 w-full flex justify-between items-center">
51
+ <span>{mergedTranslations.cardNumber}</span>
52
+ </div>
53
+
54
+ <Input
55
+ format="#### #### #### ####"
56
+ mask="_"
57
+ allowEmptyFormatting={true}
58
+ control={control}
59
+ {...register('card_number')}
60
+ error={errors.card_number}
61
+ />
62
+ </div>
63
+
64
+ <div className="w-full my-2 sm:flex">
65
+ <div className="sm:w-2/3 sm:px-4">
66
+ <label
67
+ className="flex w-full text-xs text-start text-black-400 mb-1.5"
68
+ htmlFor="card_month"
69
+ >
70
+ {mergedTranslations.expiryDate}
71
+ </label>
72
+
73
+ <div className="flex w-full h-10 space-x-2.5">
74
+ <div className="w-2/4">
75
+ <Select
76
+ className="w-full text-xs border-gray-400 sm:text-sm"
77
+ options={months}
78
+ {...register('card_month')}
79
+ error={errors.card_month}
80
+ />
81
+ </div>
82
+
83
+ <div className="w-2/4">
84
+ <Select
85
+ className="w-full text-xs border-gray-400 sm:text-sm"
86
+ options={years}
87
+ {...register('card_year')}
88
+ error={errors.card_year}
89
+ />
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <div className="my-2 sm:w-1/3 sm:px-4 sm:my-0">
95
+ <label
96
+ className="flex w-full text-xs text-start text-black-400 mb-1.5"
97
+ htmlFor="card_cvv"
98
+ >
99
+ {mergedTranslations.cvv}
100
+ </label>
101
+ <Input
102
+ format="###"
103
+ mask="_"
104
+ control={control}
105
+ allowEmptyFormatting={true}
106
+ {...register('card_cvv')}
107
+ error={errors.card_cvv}
108
+ />
109
+ <div className="group relative flex items-center justify-start text-gray-600 cursor-pointer mt-2 transition-all hover:text-secondary">
110
+ <span className="text-xs underline">
111
+ {mergedTranslations.cvvTooltip}
112
+ </span>
113
+ <Icon name="cvc" size={16} className="leading-none ml-2" />
114
+ <div className="hidden group-hover:block absolute right-0 bottom-5 w-[11rem] lg:w-[21rem] lg:left-auto lg:right-auto border-2">
115
+ <Image src="/cvv.jpg" alt="Cvv" width={385} height={264} />
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ <div id="ucs-permission"></div>
122
+ </div>
123
+ </div>
124
+ );
125
+ };
@@ -1,5 +1,4 @@
1
1
  import { setDeletionModalId, setDeletionModalVisible } from '../redux/reducer';
2
- import React from 'react';
3
2
  import { ErrorText } from './error-text';
4
3
  import { CardLabel } from './card-label';
5
4
  import { DeleteIcon } from './delete-icon';
@@ -1,19 +1,17 @@
1
1
  import SavedCardInstallments from './installments';
2
- import React from 'react';
2
+ import { InstallmentSectionProps } from '../types';
3
3
 
4
4
  export const InstallmentSection = ({
5
5
  title,
6
- selectedCard,
7
6
  installmentOptions,
8
7
  translations,
9
8
  errors
10
- }) => (
9
+ }: InstallmentSectionProps) => (
11
10
  <div className="border-solid border-gray-400 bg-white">
12
11
  <div className="px-4 py-2">
13
12
  <span className="text-black-800 text-lg font-medium">{title}</span>
14
13
  </div>
15
14
  <SavedCardInstallments
16
- selectedCard={selectedCard}
17
15
  installmentOptions={installmentOptions}
18
16
  translations={translations}
19
17
  error={errors}
@@ -12,14 +12,12 @@ const defaultTranslations = {
12
12
  };
13
13
 
14
14
  type SavedCardInstallmentsProps = {
15
- selectedCard: SavedCard;
16
15
  installmentOptions: any[];
17
16
  translations?: InstallmentTexts;
18
17
  error: any;
19
18
  };
20
19
 
21
20
  const SavedCardInstallments = ({
22
- selectedCard,
23
21
  installmentOptions = [],
24
22
  translations,
25
23
  error
@@ -30,7 +28,6 @@ const SavedCardInstallments = ({
30
28
 
31
29
  useEffect(() => {
32
30
  if (
33
- selectedCard &&
34
31
  installmentOptions.length > 0 &&
35
32
  installmentOptions[0]?.pk !== installmentOption
36
33
  ) {
@@ -40,7 +37,7 @@ const SavedCardInstallments = ({
40
37
  });
41
38
  setInstallmentOption(firstOptionPk);
42
39
  }
43
- }, [installmentOptions, installmentOption, setInstallment, selectedCard]);
40
+ }, [installmentOptions, installmentOption, setInstallment]);
44
41
 
45
42
  if (installmentOptions.length === 0) {
46
43
  return (
package/src/index.tsx CHANGED
@@ -1,5 +1,11 @@
1
1
  import savedCardReducer from './redux/reducer';
2
2
  import savedCardMiddleware from './redux/middleware';
3
3
  import SavedCardOption from './views/saved-card-option';
4
+ import IyzicoSavedCardOption from './views/iyzico-saved-card-option';
4
5
 
5
- export { savedCardReducer, savedCardMiddleware, SavedCardOption };
6
+ export {
7
+ savedCardReducer,
8
+ savedCardMiddleware,
9
+ SavedCardOption,
10
+ IyzicoSavedCardOption
11
+ };
package/src/redux/api.ts CHANGED
@@ -21,20 +21,48 @@ interface GetResponse<T> {
21
21
  results: T[];
22
22
  }
23
23
 
24
- type SetSavedCardRequest = {
24
+ type DefaultSetSavedCardRequest = {
25
25
  card: SavedCard;
26
26
  };
27
27
 
28
+ type IyzicoSetSavedCardRequest = {
29
+ card: string;
30
+ };
31
+
32
+ type SetSavedCardRequest =
33
+ | DefaultSetSavedCardRequest
34
+ | IyzicoSetSavedCardRequest;
35
+
28
36
  type DeleteSavedCardRequest = number;
29
37
 
30
38
  type SetInstallmentRequest = {
31
39
  installment: string;
32
40
  };
33
41
 
34
- type CompleteSavedCardRequest = {
42
+ type DefaultCompleteSavedCardRequest = {
35
43
  agreement: boolean;
36
44
  };
37
45
 
46
+ type IyzicoCompleteSavedCardRequest =
47
+ | {
48
+ agreement: boolean;
49
+ consumer_token: string;
50
+ card_token: string;
51
+ }
52
+ | {
53
+ agreement: boolean;
54
+ card_holder: string;
55
+ card_number: string;
56
+ card_cvv: string;
57
+ card_month: string;
58
+ card_year: string;
59
+ register_consumer_card: boolean;
60
+ };
61
+
62
+ type CompleteSavedCardRequest =
63
+ | DefaultCompleteSavedCardRequest
64
+ | IyzicoCompleteSavedCardRequest;
65
+
38
66
  export const savedCardApi = api.injectEndpoints({
39
67
  endpoints: (build) => ({
40
68
  getSavedCards: build.query<GetResponse<SavedCard>, void>({
@@ -71,7 +99,7 @@ export const savedCardApi = api.injectEndpoints({
71
99
  ),
72
100
  method: 'POST',
73
101
  body: {
74
- card: card.token
102
+ card: typeof card === 'string' ? card : card.token
75
103
  }
76
104
  }),
77
105
  async onQueryStarted({ card }, { dispatch, queryFulfilled }) {
@@ -106,7 +134,7 @@ export const savedCardApi = api.injectEndpoints({
106
134
  CheckoutResponse,
107
135
  CompleteSavedCardRequest
108
136
  >({
109
- query: ({ agreement }) => ({
137
+ query: (body) => ({
110
138
  url: buildClientRequestUrl(
111
139
  `/orders/checkout?page=CompleteSavedCardRequest`,
112
140
  {
@@ -114,9 +142,7 @@ export const savedCardApi = api.injectEndpoints({
114
142
  }
115
143
  ),
116
144
  method: 'POST',
117
- body: {
118
- agreement
119
- }
145
+ body
120
146
  }),
121
147
  async onQueryStarted(arg, { dispatch, queryFulfilled }) {
122
148
  dispatch(setPaymentStepBusy(true));
@@ -1,22 +1,27 @@
1
1
  import { Middleware } from '@reduxjs/toolkit';
2
- import { setCards } from './reducer';
2
+ import { setCards, setUcs } from './reducer';
3
3
 
4
4
  const savedCardMiddleware: Middleware = ({ dispatch }) => {
5
5
  return (next) => (action) => {
6
6
  const result = next(action);
7
7
 
8
8
  const savedCardContext = result.payload?.context_list?.find(
9
- (context) =>
10
- context.page_slug === 'savedcardselectionpage' &&
11
- context.page_context.cards
9
+ (context) => context.page_slug === 'savedcardselectionpage'
12
10
  );
13
11
 
14
- if (savedCardContext) {
12
+ if (savedCardContext && savedCardContext.page_context?.cards) {
15
13
  const cards = JSON.parse(
16
14
  JSON.stringify(savedCardContext.page_context.cards)
17
15
  );
18
16
 
19
17
  dispatch(setCards(cards));
18
+ } else if (savedCardContext && savedCardContext.page_context?.ucs) {
19
+ const ucs = JSON.parse(JSON.stringify(savedCardContext.page_context.ucs));
20
+
21
+ const match = ucs.script.match(/<script\b[^>]*>([\s\S]*?)<\/script>/);
22
+ ucs.script = match ? match[1] : ucs.script;
23
+
24
+ dispatch(setUcs(ucs));
20
25
  }
21
26
 
22
27
  return result;
@@ -7,12 +7,20 @@ export type SavedCard = {
7
7
  token: string;
8
8
  };
9
9
 
10
+ export type IyzicoUcs = {
11
+ maskedGsmNumber?: string;
12
+ script?: string;
13
+ scriptType?: string;
14
+ ucsToken?: string;
15
+ };
16
+
10
17
  export interface SavedCardState {
11
18
  cards?: Array<SavedCard>;
12
19
  deletion: {
13
20
  id: number;
14
21
  isModalVisible: boolean;
15
22
  };
23
+ ucs?: IyzicoUcs;
16
24
  }
17
25
 
18
26
  const initialState: SavedCardState = {
@@ -35,11 +43,22 @@ const savedCardSlice = createSlice({
35
43
  },
36
44
  setDeletionModalVisible(state, { payload }) {
37
45
  state.deletion.isModalVisible = payload;
46
+ },
47
+ setUcs(state, action: { payload: IyzicoUcs }) {
48
+ state.ucs = action.payload;
49
+ },
50
+ resetUcs() {
51
+ return initialState;
38
52
  }
39
53
  }
40
54
  });
41
55
 
42
- export const { setCards, setDeletionModalId, setDeletionModalVisible } =
43
- savedCardSlice.actions;
56
+ export const {
57
+ setCards,
58
+ setDeletionModalId,
59
+ setDeletionModalVisible,
60
+ setUcs,
61
+ resetUcs
62
+ } = savedCardSlice.actions;
44
63
 
45
64
  export default savedCardSlice.reducer;
@@ -1,4 +1,5 @@
1
1
  import { ReactElement } from 'react';
2
+ import { Control, FieldErrors } from 'react-hook-form';
2
3
 
3
4
  export type InstallmentTexts = {
4
5
  title?: string;
@@ -15,6 +16,9 @@ export type DeletePopupTexts = {
15
16
 
16
17
  export type ErrorTexts = {
17
18
  required?: string;
19
+ cardNumberLength?: string;
20
+ cvvLength?: string;
21
+ cardHolderMatches?: string;
18
22
  };
19
23
 
20
24
  export type SavedCardOptionTexts = {
@@ -25,6 +29,28 @@ export type SavedCardOptionTexts = {
25
29
  errors?: ErrorTexts;
26
30
  };
27
31
 
32
+ export type IyzicoSavedCardOptionTexts = {
33
+ title?: string;
34
+ button?: string;
35
+ installment?: InstallmentTexts;
36
+ errors?: ErrorTexts & {
37
+ saveCardRequired?: string;
38
+ };
39
+ label?: {
40
+ cardHolder?: string;
41
+ cardNumber?: string;
42
+ expiryDate?: string;
43
+ cvv?: string;
44
+ };
45
+ placeholder?: {
46
+ cardHolder?: string;
47
+ cardMonth?: string;
48
+ cardYear?: string;
49
+ };
50
+ savedCardsTitle?: string;
51
+ paymentErrorMessage?: string;
52
+ };
53
+
28
54
  export type CardSelectionSectionProps = {
29
55
  title: string;
30
56
  cards: any[];
@@ -37,15 +63,34 @@ export type CardSelectionSectionProps = {
37
63
 
38
64
  export type InstallmentSectionProps = {
39
65
  title: string;
40
- selectedCard: any;
41
66
  installmentOptions: any;
42
67
  translations: InstallmentTexts;
43
68
  errors: any;
69
+ setFormValue?: any;
44
70
  };
45
71
 
46
- export type AgreementAndSubmitProps = {
47
- agreementCheckbox: ReactElement | undefined;
72
+ export interface AgreementAndSubmitProps {
73
+ agreementCheckbox?: ReactElement;
74
+ control: Control<any>;
75
+ errors: FieldErrors;
76
+ buttonText: string;
77
+ formError?: {
78
+ non_field_errors?: string;
79
+ status?: string;
80
+ [key: string]: any;
81
+ };
82
+ }
83
+
84
+ export type CardFormSectionProps = {
85
+ register: any;
48
86
  control: any;
49
87
  errors: any;
50
- buttonText: string;
88
+ months: Array<{ label: string; value: string }>;
89
+ years: Array<{ label: string; value: string }>;
90
+ translations?: IyzicoSavedCardOptionTexts['label'];
91
+ setFormValue?: any;
92
+ };
93
+
94
+ export type IyzicoSavedCardsProps = {
95
+ title?: string;
51
96
  };
@@ -0,0 +1,438 @@
1
+ 'use client';
2
+
3
+ import * as yup from 'yup';
4
+ import { ObjectSchema } from 'yup';
5
+ import { yupResolver } from '@hookform/resolvers/yup';
6
+ import { useForm } from 'react-hook-form';
7
+ import {
8
+ FormHTMLAttributes,
9
+ HTMLAttributes,
10
+ ReactElement,
11
+ useEffect,
12
+ useMemo,
13
+ useState
14
+ } from 'react';
15
+ import { useAppSelector } from '@akinon/next/redux/hooks';
16
+ import {
17
+ useCompleteSavedCardMutation,
18
+ useSetSavedCardMutation
19
+ } from '../redux/api';
20
+
21
+ import {
22
+ AgreementAndSubmitProps,
23
+ CardFormSectionProps,
24
+ ErrorTexts,
25
+ InstallmentSectionProps,
26
+ IyzicoSavedCardOptionTexts,
27
+ IyzicoSavedCardsProps
28
+ } from '../types';
29
+ import { InstallmentSection } from '../components/installment-section';
30
+ import { AgreementAndSubmit } from '../components/agreement-and-submit';
31
+ import { CardFormSection } from '../components/card-form-section';
32
+ import Script from 'next/script';
33
+
34
+ type IyzicoSavedCardOptionProps = {
35
+ texts?: IyzicoSavedCardOptionTexts;
36
+ agreementCheckbox?: ReactElement;
37
+ customFormSchema?: {
38
+ 'new-card'?: ObjectSchema<any>;
39
+ 'existing-card'?: ObjectSchema<any>;
40
+ };
41
+ customRender?: {
42
+ cardFormSection?: (props: CardFormSectionProps) => ReactElement;
43
+ savedCardsSection?: (props: IyzicoSavedCardsProps) => ReactElement;
44
+ installmentSection?: (props: InstallmentSectionProps) => ReactElement;
45
+ agreementAndSubmit?: (props: AgreementAndSubmitProps) => ReactElement;
46
+ };
47
+ formWrapperClassName?: string;
48
+ cardSelectionWrapperClassName?: string;
49
+ installmentWrapperClassName?: string;
50
+ formProps?: FormHTMLAttributes<HTMLFormElement>;
51
+ cardSelectionWrapperProps?: HTMLAttributes<HTMLDivElement>;
52
+ installmentWrapperProps?: HTMLAttributes<HTMLDivElement>;
53
+ };
54
+
55
+ const defaultTranslations: IyzicoSavedCardOptionTexts = {
56
+ title: 'Pay with Iyzico',
57
+ button: 'Complete Payment',
58
+ installment: {
59
+ title: 'Installment Options'
60
+ },
61
+ errors: {
62
+ required: 'This field is required',
63
+ cardNumberLength: 'Card number must be 16 digits long.',
64
+ cvvLength: 'CVV must be 3 digits long.',
65
+ cardHolderMatches: 'Please enter a valid name',
66
+ saveCardRequired: 'Please select the Save My Card option'
67
+ },
68
+ label: {
69
+ cardHolder: 'Cardholder Name',
70
+ cardNumber: 'Card Number',
71
+ expiryDate: 'Expiration Date',
72
+ cvv: 'CVV'
73
+ },
74
+ placeholder: {
75
+ cardHolder: 'Name on Card',
76
+ cardMonth: 'Month',
77
+ cardYear: 'Year'
78
+ },
79
+ savedCardsTitle: 'Select from your saved cards:',
80
+ paymentErrorMessage: 'An error occurred while processing your payment'
81
+ };
82
+
83
+ const mergeTranslations = (
84
+ customTranslations: IyzicoSavedCardOptionTexts,
85
+ defaultTranslations: IyzicoSavedCardOptionTexts
86
+ ) => {
87
+ return {
88
+ ...defaultTranslations,
89
+ ...customTranslations,
90
+ installment: {
91
+ ...defaultTranslations.installment,
92
+ ...customTranslations.installment
93
+ },
94
+ errors: {
95
+ ...defaultTranslations.errors,
96
+ ...customTranslations.errors
97
+ },
98
+ label: {
99
+ ...defaultTranslations.label,
100
+ ...customTranslations.label
101
+ },
102
+ placeholder: {
103
+ ...defaultTranslations.placeholder,
104
+ ...customTranslations.placeholder
105
+ }
106
+ };
107
+ };
108
+
109
+ const createFormSchema = (
110
+ errors: ErrorTexts,
111
+ type: 'existing-card' | 'new-card'
112
+ ) => {
113
+ const baseSchema = {
114
+ agreement: yup
115
+ .boolean()
116
+ .oneOf([true], errors?.required ?? defaultTranslations.errors.required)
117
+ };
118
+
119
+ if (type === 'new-card') {
120
+ return yup.object().shape({
121
+ ...baseSchema,
122
+ card_holder: yup
123
+ .string()
124
+ .required(errors?.required ?? defaultTranslations.errors.required)
125
+ .matches(
126
+ /^[a-zA-ZğüşöçıİĞÜŞÖÇ\s]+$/,
127
+ errors?.cardHolderMatches ??
128
+ defaultTranslations.errors.cardHolderMatches
129
+ ),
130
+ card_number: yup
131
+ .string()
132
+ .transform((value: string) => value.replace(/_/g, '').replace(/ /g, ''))
133
+ .length(
134
+ 16,
135
+ errors?.cardNumberLength ??
136
+ defaultTranslations.errors.cardNumberLength
137
+ )
138
+ .required(errors?.required ?? defaultTranslations.errors.required),
139
+ card_month: yup
140
+ .string()
141
+ .required(errors?.required ?? defaultTranslations.errors.required),
142
+ card_year: yup
143
+ .string()
144
+ .required(errors?.required ?? defaultTranslations.errors.required),
145
+ card_cvv: yup
146
+ .string()
147
+ .transform((value: string) => value.replace(/_/g, '').replace(/ /g, ''))
148
+ .length(3, errors?.cvvLength ?? defaultTranslations.errors.cvvLength)
149
+ .required(errors?.required ?? defaultTranslations.errors.required)
150
+ });
151
+ }
152
+
153
+ return yup.object().shape(baseSchema);
154
+ };
155
+
156
+ const IyzicoSavedCardOption = ({
157
+ texts = defaultTranslations,
158
+ agreementCheckbox,
159
+ customFormSchema,
160
+ customRender,
161
+ formWrapperClassName = 'flex flex-wrap w-full',
162
+ cardSelectionWrapperClassName = 'w-full flex flex-col xl:w-6/10',
163
+ installmentWrapperClassName = 'w-full xl:w-4/10 xl:border-l xl:border-t-0',
164
+ formProps = {},
165
+ cardSelectionWrapperProps = {},
166
+ installmentWrapperProps = {}
167
+ }: IyzicoSavedCardOptionProps) => {
168
+ const mergedTexts = useMemo(
169
+ () => mergeTranslations(texts, defaultTranslations),
170
+ [texts]
171
+ );
172
+ const [completeSavedCard] = useCompleteSavedCardMutation();
173
+ const [setSavedCard] = useSetSavedCardMutation();
174
+ const installmentOptions = useAppSelector(
175
+ (state) => state.checkout.installmentOptions
176
+ );
177
+ const ucs = useAppSelector((state) => state.savedCard.ucs);
178
+ const [type, setType] = useState<'new-card' | 'existing-card'>('new-card');
179
+ const [months, setMonths] = useState([]);
180
+ const [years, setYears] = useState([]);
181
+ const [formError, setFormError] = useState(null);
182
+
183
+ const getFormSchema = () => {
184
+ if (customFormSchema?.[type]) {
185
+ return customFormSchema[type];
186
+ }
187
+ return createFormSchema(mergedTexts.errors, type);
188
+ };
189
+
190
+ const {
191
+ register,
192
+ handleSubmit,
193
+ control,
194
+ formState: { errors },
195
+ setValue: setFormValue
196
+ } = useForm({
197
+ resolver: yupResolver(getFormSchema())
198
+ });
199
+
200
+ const onSubmit = async (data) => {
201
+ if (!window.iyziUcsInit) {
202
+ return;
203
+ }
204
+
205
+ setFormError(null);
206
+
207
+ try {
208
+ if (type === 'existing-card') {
209
+ const res = await completeSavedCard({
210
+ agreement: data.agreement,
211
+ consumer_token: window.universalCardStorage.consumerToken,
212
+ card_token: window.universalCardStorage.cardToken
213
+ });
214
+
215
+ if ('data' in res && res.data?.errors) {
216
+ setFormError(res.data.errors);
217
+ }
218
+ } else {
219
+ if (!window.universalCardStorage?.registerConsumerCard) {
220
+ setFormError({
221
+ register_consumer_card: mergedTexts.errors?.saveCardRequired
222
+ });
223
+ return;
224
+ }
225
+
226
+ const res = await completeSavedCard({
227
+ agreement: data.agreement,
228
+ card_holder: data.card_holder,
229
+ card_number: data.card_number.replaceAll(' ', ''),
230
+ card_month: data.card_month,
231
+ card_year: data.card_year,
232
+ card_cvv: data.card_cvv,
233
+ register_consumer_card:
234
+ window.universalCardStorage.registerConsumerCard
235
+ });
236
+
237
+ if ('data' in res && res.data?.errors) {
238
+ setFormError(res.data.errors);
239
+ }
240
+ }
241
+ } catch (error) {
242
+ console.error('Error completing saved card:', error);
243
+ setFormError({
244
+ non_field_errors:
245
+ mergedTexts.paymentErrorMessage ??
246
+ defaultTranslations.paymentErrorMessage
247
+ });
248
+ }
249
+ };
250
+
251
+ const ucsScript = ucs?.script && (
252
+ <Script
253
+ id="saved_card"
254
+ strategy="afterInteractive"
255
+ dangerouslySetInnerHTML={{ __html: ucs.script }}
256
+ />
257
+ );
258
+
259
+ useEffect(() => {
260
+ window.iyziUcsInit?.createTag();
261
+
262
+ const monthsList = [
263
+ {
264
+ label:
265
+ mergedTexts.placeholder?.cardMonth ??
266
+ defaultTranslations.placeholder.cardMonth,
267
+ value: ''
268
+ }
269
+ ];
270
+ const yearsList = [
271
+ {
272
+ label:
273
+ mergedTexts.placeholder?.cardYear ??
274
+ defaultTranslations.placeholder.cardYear,
275
+ value: ''
276
+ }
277
+ ];
278
+ const date = new Date();
279
+ const currentYear = date.getFullYear();
280
+
281
+ for (let i = 1; i <= 12; i++) {
282
+ monthsList.push({ label: i.toString(), value: i.toString() });
283
+ }
284
+
285
+ for (let i = currentYear; i < currentYear + 13; i++) {
286
+ yearsList.push({ label: i.toString(), value: i.toString() });
287
+ }
288
+
289
+ setMonths(monthsList);
290
+ setYears(yearsList);
291
+
292
+ if (window.iyziUcsInit?.ucsToken) {
293
+ setSavedCard({
294
+ card: window.iyziUcsInit.ucsToken
295
+ });
296
+ }
297
+ }, []);
298
+
299
+ useEffect(() => {
300
+ if (window.iyziUcsInit?.scriptType === 'CONSUMER_WITH_CARD_EXIST') {
301
+ if (
302
+ !window.universalCardStorage?.consumerToken ||
303
+ window.universalCardStorage?.cardToken
304
+ ) {
305
+ setType('existing-card');
306
+ } else {
307
+ setType('new-card');
308
+ }
309
+ } else {
310
+ setType('new-card');
311
+ }
312
+
313
+ const checkInterval = setInterval(() => {
314
+ if (window.iyziUcsInit?.scriptType === 'CONSUMER_WITH_CARD_EXIST') {
315
+ if (
316
+ !window.universalCardStorage?.consumerToken ||
317
+ window.universalCardStorage?.cardToken
318
+ ) {
319
+ setType('existing-card');
320
+ } else {
321
+ setType('new-card');
322
+ }
323
+ }
324
+ }, 500);
325
+
326
+ return () => clearInterval(checkInterval);
327
+ }, [type]);
328
+
329
+ return (
330
+ <>
331
+ <form
332
+ className={formWrapperClassName}
333
+ onSubmit={handleSubmit(onSubmit)}
334
+ {...formProps}
335
+ >
336
+ <div
337
+ className={cardSelectionWrapperClassName}
338
+ {...cardSelectionWrapperProps}
339
+ >
340
+ {mergedTexts.title && (
341
+ <div className="border-solid border-gray-400 px-4 py-2">
342
+ <span className="text-black-800 text-lg font-medium">
343
+ {mergedTexts.title}
344
+ </span>
345
+ </div>
346
+ )}
347
+
348
+ {window.iyziUcsInit?.scriptType === 'CONSUMER_WITH_CARD_EXIST' && (
349
+ <>
350
+ {customRender?.savedCardsSection ? (
351
+ customRender.savedCardsSection({
352
+ title: mergedTexts.savedCardsTitle
353
+ })
354
+ ) : (
355
+ <div>
356
+ <h3 className="text-sm font-medium text-gray-800 mb-3 px-4 sm:px-6">
357
+ {mergedTexts.savedCardsTitle}
358
+ </h3>
359
+ <div id="ucs-cards" className="mb-3 px-4 sm:px-6"></div>
360
+ </div>
361
+ )}
362
+ </>
363
+ )}
364
+
365
+ {type === 'new-card' && (
366
+ <>
367
+ {customRender?.cardFormSection ? (
368
+ customRender.cardFormSection({
369
+ register,
370
+ control,
371
+ errors,
372
+ months,
373
+ years,
374
+ translations: mergedTexts.label,
375
+ setFormValue
376
+ })
377
+ ) : (
378
+ <CardFormSection
379
+ register={register}
380
+ control={control}
381
+ errors={errors}
382
+ months={months}
383
+ years={years}
384
+ translations={mergedTexts.label}
385
+ />
386
+ )}
387
+ </>
388
+ )}
389
+ </div>
390
+
391
+ <div
392
+ className={installmentWrapperClassName}
393
+ {...installmentWrapperProps}
394
+ >
395
+ {customRender?.installmentSection ? (
396
+ customRender.installmentSection({
397
+ title: mergedTexts.installment?.title,
398
+ installmentOptions,
399
+ translations: mergedTexts.installment,
400
+ errors: errors.installment,
401
+ setFormValue
402
+ })
403
+ ) : (
404
+ <InstallmentSection
405
+ title={mergedTexts.installment?.title}
406
+ installmentOptions={installmentOptions}
407
+ translations={mergedTexts.installment}
408
+ errors={errors.installment}
409
+ />
410
+ )}
411
+
412
+ <div className="flex flex-col text-xs">
413
+ {customRender?.agreementAndSubmit ? (
414
+ customRender.agreementAndSubmit({
415
+ agreementCheckbox,
416
+ control,
417
+ errors,
418
+ buttonText: mergedTexts.button,
419
+ formError
420
+ })
421
+ ) : (
422
+ <AgreementAndSubmit
423
+ agreementCheckbox={agreementCheckbox}
424
+ control={control}
425
+ errors={errors}
426
+ buttonText={mergedTexts.button}
427
+ formError={formError}
428
+ />
429
+ )}
430
+ </div>
431
+ </div>
432
+ </form>
433
+ {ucsScript}
434
+ </>
435
+ );
436
+ };
437
+
438
+ export default IyzicoSavedCardOption;
@@ -183,7 +183,6 @@ const SavedCardOption = ({
183
183
  {customRender?.installmentSection ? (
184
184
  customRender.installmentSection({
185
185
  title: mergedTexts.installment?.title,
186
- selectedCard,
187
186
  installmentOptions,
188
187
  translations: mergedTexts.installment,
189
188
  errors: errors.installment
@@ -191,7 +190,6 @@ const SavedCardOption = ({
191
190
  ) : (
192
191
  <InstallmentSection
193
192
  title={mergedTexts.installment?.title}
194
- selectedCard={selectedCard}
195
193
  installmentOptions={installmentOptions}
196
194
  translations={mergedTexts.installment}
197
195
  errors={errors.installment}