@akinon/pz-saved-card 1.59.0-rc.4 → 1.60.0-rc.10

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -1,7 +1,39 @@
1
1
  # @akinon/pz-saved-card
2
2
 
3
+ ## 1.60.0-rc.10
4
+
5
+ ## 1.60.0-rc.9
6
+
7
+ ## 1.60.0-rc.8
8
+
9
+ ## 1.60.0-rc.7
10
+
11
+ ### Minor Changes
12
+
13
+ - 63597bc: ZERO-2903: update saved card readme
14
+ - ce25dac: ZERO-2903: optimize saved card
15
+
16
+ ## 1.60.0-rc.6
17
+
18
+ ### Minor Changes
19
+
20
+ - b92001c: ZERO-2903: fix missing dependency in useEffect hook
21
+ - 8fb37c4: ZERO-2903: enchance SavedCardOption component with custom wrapper props and classnames
22
+
23
+ ## 1.59.0-rc.5
24
+
25
+ ### Minor Changes
26
+
27
+ - b92001c: ZERO-2903: fix missing dependency in useEffect hook
28
+ - 8fb37c4: ZERO-2903: enchance SavedCardOption component with custom wrapper props and classnames
29
+
3
30
  ## 1.59.0-rc.4
4
31
 
32
+ ### Minor Changes
33
+
34
+ - b92001c: ZERO-2903: fix missing dependency in useEffect hook
35
+ - 8fb37c4: ZERO-2903: enchance SavedCardOption component with custom wrapper props and classnames
36
+
5
37
  ## 1.59.0-rc.3
6
38
 
7
39
  ## 1.59.0-rc.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/pz-saved-card",
3
- "version": "1.59.0-rc.4",
3
+ "version": "1.60.0-rc.10",
4
4
  "license": "MIT",
5
5
  "main": "src/index.tsx",
6
6
  "peerDependencies": {
package/readme.md CHANGED
@@ -25,17 +25,20 @@ npx @akinon/projectzero@latest --plugins
25
25
  ##### File Path: src/views/checkout/steps/payment/options/saved-card.tsx
26
26
 
27
27
  ```jsx
28
- import { SavedCardOption } from '@akinon/pz-saved-card';
28
+ import PluginModule, { Component } from '@akinon/next/components/plugin-module';
29
29
 
30
30
  const SavedCard = () => {
31
- return (
32
- <SavedCardOption
33
- texts={{
34
- title: 'Pay with Saved Card',
35
- button: 'Pay Now'
36
- }}
37
- />
38
- );
31
+ return (
32
+ <PluginModule
33
+ component={Component.SavedCard}
34
+ props={{
35
+ texts: {
36
+ title: 'Pay with Saved Card',
37
+ button: 'Pay Now'
38
+ }
39
+ }}
40
+ />
41
+ );
39
42
  };
40
43
 
41
44
  export default SavedCard;
@@ -0,0 +1,25 @@
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
+ );
@@ -0,0 +1,17 @@
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
+ );
@@ -0,0 +1,51 @@
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
+ );
@@ -0,0 +1,8 @@
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
+ );
@@ -0,0 +1,7 @@
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
+ );
@@ -0,0 +1,22 @@
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
+ );
@@ -40,7 +40,7 @@ const SavedCardInstallments = ({
40
40
  });
41
41
  setInstallmentOption(firstOptionPk);
42
42
  }
43
- }, [installmentOptions, installmentOption, setInstallment]);
43
+ }, [installmentOptions, installmentOption, setInstallment, selectedCard]);
44
44
 
45
45
  if (installmentOptions.length === 0) {
46
46
  return (
@@ -47,6 +47,5 @@ export type AgreementAndSubmitProps = {
47
47
  agreementCheckbox: ReactElement | undefined;
48
48
  control: any;
49
49
  errors: any;
50
- formError: any;
51
50
  buttonText: string;
52
51
  };
@@ -3,29 +3,20 @@
3
3
  import * as yup from 'yup';
4
4
  import { yupResolver } from '@hookform/resolvers/yup';
5
5
  import { useForm } from 'react-hook-form';
6
- import React, { cloneElement, ReactElement, useEffect, useState } from 'react';
6
+ import React, { ReactElement, useEffect, useMemo } from 'react';
7
7
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
8
- import { RootState } from 'projectzeronext/src/redux/store';
9
8
  import {
10
9
  useCompleteSavedCardMutation,
11
10
  useGetSavedCardsQuery,
12
11
  useSetSavedCardMutation
13
12
  } from '../redux/api';
14
- import { Button, Icon, Image } from '@akinon/next/components';
15
- import { getCreditCardType } from '../utils';
16
13
 
17
14
  import amex from '../../assets/img/amex.jpg';
18
15
  import mastercard from '../../assets/img/mastercard.png';
19
16
  import other from '../../assets/img/other.png';
20
17
  import troy from '../../assets/img/troy.png';
21
18
  import visa from '../../assets/img/visa.png';
22
-
23
- import SavedCardInstallments from '../components/installments';
24
- import {
25
- setCards,
26
- setDeletionModalId,
27
- setDeletionModalVisible
28
- } from '../redux/reducer';
19
+ import { setCards } from '../redux/reducer';
29
20
  import { DeleteConfirmationModal } from '../components/delete-confirmation-modal';
30
21
  import {
31
22
  AgreementAndSubmitProps,
@@ -34,8 +25,11 @@ import {
34
25
  InstallmentSectionProps,
35
26
  SavedCardOptionTexts
36
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';
37
31
 
38
- const cardImages = { amex, mastercard, troy, visa, other };
32
+ export const cardImages = { amex, mastercard, troy, visa, other };
39
33
 
40
34
  type SavedCardOptionProps = {
41
35
  texts?: SavedCardOptionTexts;
@@ -45,6 +39,12 @@ type SavedCardOptionProps = {
45
39
  installmentSection?: (props: InstallmentSectionProps) => ReactElement;
46
40
  agreementAndSubmit?: (props: AgreementAndSubmitProps) => ReactElement;
47
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
48
  };
49
49
 
50
50
  const defaultTranslations: SavedCardOptionTexts = {
@@ -94,40 +94,54 @@ const createFormSchema = (errors: ErrorTexts) =>
94
94
  const SavedCardOption = ({
95
95
  texts = defaultTranslations,
96
96
  agreementCheckbox,
97
- customRender
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 = {}
98
104
  }: SavedCardOptionProps) => {
99
- const mergedTexts = mergeTranslations(texts, defaultTranslations);
100
-
101
- const [selectedCard, setSelectedCard] = useState(null);
102
- const [formError, setFormError] = useState(null);
105
+ const mergedTexts = useMemo(
106
+ () => mergeTranslations(texts, defaultTranslations),
107
+ [texts]
108
+ );
103
109
  const dispatch = useAppDispatch();
104
110
  const { data: savedCards } = useGetSavedCardsQuery();
105
111
  const [completeSavedCard] = useCompleteSavedCardMutation();
106
112
  const [setSavedCard] = useSetSavedCardMutation();
107
113
  const installmentOptions = useAppSelector(
108
- (state: RootState) => state.checkout.installmentOptions
114
+ (state) => state.checkout.installmentOptions
109
115
  );
110
- const cards = useAppSelector((state: RootState) => state.savedCard.cards);
116
+ const cards = useAppSelector((state) => state.savedCard.cards);
111
117
 
112
118
  const {
113
119
  register,
114
120
  handleSubmit,
115
121
  control,
116
- formState: { errors }
122
+ formState: { errors },
123
+ setValue,
124
+ watch
117
125
  } = useForm({
118
126
  resolver: yupResolver(createFormSchema(mergedTexts.errors))
119
127
  });
120
128
 
129
+ const selectedCardToken = watch('card');
130
+ const selectedCard = useMemo(
131
+ () => cards?.find((card) => card.token === selectedCardToken),
132
+ [cards, selectedCardToken]
133
+ );
134
+
121
135
  const handleCardSelection = async (card) => {
122
136
  await setSavedCard({ card }).unwrap();
123
- setSelectedCard(card);
137
+ setValue('card', card.token, { shouldValidate: true });
124
138
  };
125
139
 
126
140
  const onSubmit = async () => {
127
141
  try {
128
142
  await completeSavedCard({ agreement: true });
129
143
  } catch (error) {
130
- setFormError(error);
144
+ console.error('Error completing saved card:', error);
131
145
  }
132
146
  };
133
147
 
@@ -135,12 +149,19 @@ const SavedCardOption = ({
135
149
  if (savedCards?.results && !cards?.length) {
136
150
  dispatch(setCards(savedCards.results));
137
151
  }
138
- }, [savedCards]);
152
+ }, [savedCards, cards, dispatch]);
139
153
 
140
154
  return (
141
155
  <>
142
- <form className="flex flex-wrap w-full" onSubmit={handleSubmit(onSubmit)}>
143
- <div className="w-full flex flex-col xl:w-6/10">
156
+ <form
157
+ className={formWrapperClassName}
158
+ onSubmit={handleSubmit(onSubmit)}
159
+ {...formProps}
160
+ >
161
+ <div
162
+ className={cardSelectionWrapperClassName}
163
+ {...cardSelectionWrapperProps}
164
+ >
144
165
  {customRender?.cardSelectionSection ? (
145
166
  customRender.cardSelectionSection({
146
167
  title: mergedTexts.title,
@@ -164,7 +185,10 @@ const SavedCardOption = ({
164
185
  )}
165
186
  </div>
166
187
 
167
- <div className="w-full xl:w-4/10 xl:border-l xl:border-t-0">
188
+ <div
189
+ className={installmentWrapperClassName}
190
+ {...installmentWrapperProps}
191
+ >
168
192
  {customRender?.installmentSection ? (
169
193
  customRender.installmentSection({
170
194
  title: mergedTexts.installment?.title,
@@ -188,7 +212,6 @@ const SavedCardOption = ({
188
212
  agreementCheckbox,
189
213
  control,
190
214
  errors,
191
- formError,
192
215
  buttonText: mergedTexts.button
193
216
  })
194
217
  ) : (
@@ -196,7 +219,6 @@ const SavedCardOption = ({
196
219
  agreementCheckbox={agreementCheckbox}
197
220
  control={control}
198
221
  errors={errors}
199
- formError={formError}
200
222
  buttonText={mergedTexts.button}
201
223
  />
202
224
  )}
@@ -207,118 +229,4 @@ const SavedCardOption = ({
207
229
  );
208
230
  };
209
231
 
210
- const CardSelectionSection = ({
211
- title,
212
- cards,
213
- selectedCard,
214
- onSelect,
215
- register,
216
- errors,
217
- dispatch
218
- }) => (
219
- <div className="border-solid border-gray-400 px-4 py-2">
220
- <span className="text-black-800 text-lg font-medium">{title}</span>
221
- <ul className="mt-4 text-xs w-full">
222
- {cards?.map((card) => (
223
- <li
224
- key={card.token}
225
- className="p-4 mb-2 border-2 border-gray-200 flex justify-between items-center cursor-pointer"
226
- onClick={() => onSelect(card)}
227
- >
228
- <input
229
- name="card"
230
- type="radio"
231
- checked={selectedCard?.token === card.token}
232
- value={card.token}
233
- id={card.token}
234
- className="mr-2"
235
- onChange={() => {}}
236
- {...register('card')}
237
- />
238
- <CardLabel card={card} />
239
- <DeleteIcon
240
- onClick={() => {
241
- dispatch(setDeletionModalId(card.id));
242
- dispatch(setDeletionModalVisible(true));
243
- }}
244
- />
245
- </li>
246
- ))}
247
- </ul>
248
- {errors.card && <ErrorText message={errors.card?.message} />}
249
- </div>
250
- );
251
-
252
- const CardLabel = ({ card }) => (
253
- <label className="flex flex-col w-full cursor-pointer md:flex-row md:items-center md:justify-between">
254
- <p className="w-full text-[10px] lg:w-1/3">{card.masked_card_number}</p>
255
- <Image
256
- className="w-8 h-6 object-contain flex items-center justify-center"
257
- width={50}
258
- height={50}
259
- src={cardImages[getCreditCardType(card.masked_card_number)].src}
260
- alt={card.name}
261
- />
262
- </label>
263
- );
264
-
265
- const DeleteIcon = ({ onClick }) => (
266
- <span className="cursor-pointer p-1" onClick={onClick}>
267
- <Icon name="close" size={12} />
268
- </span>
269
- );
270
-
271
- const InstallmentSection = ({
272
- title,
273
- selectedCard,
274
- installmentOptions,
275
- translations,
276
- errors
277
- }) => (
278
- <div className="border-solid border-gray-400 bg-white">
279
- <div className="px-4 py-2">
280
- <span className="text-black-800 text-lg font-medium">{title}</span>
281
- </div>
282
- <SavedCardInstallments
283
- selectedCard={selectedCard}
284
- installmentOptions={installmentOptions}
285
- translations={translations}
286
- error={errors}
287
- />
288
- </div>
289
- );
290
-
291
- const AgreementAndSubmit = ({
292
- agreementCheckbox,
293
- control,
294
- errors,
295
- formError,
296
- buttonText
297
- }) => (
298
- <div className="flex flex-col text-xs pb-4 px-4 sm:px-6">
299
- {agreementCheckbox &&
300
- cloneElement(agreementCheckbox, {
301
- control,
302
- error: errors.agreement,
303
- fieldId: 'agreement'
304
- })}
305
- {formError && (
306
- <ErrorText message={formError.non_field_errors ?? formError.status} />
307
- )}
308
- <Button
309
- type="submit"
310
- className="group uppercase mt-4 inline-flex items-center justify-center"
311
- >
312
- <span>{buttonText}</span>
313
- <Icon name="chevron-end" size={12} className="ml-2 h-3" />
314
- </Button>
315
- </div>
316
- );
317
-
318
- const ErrorText = ({ message }) => (
319
- <div className="w-full text-xs text-start px-1 mt-3 text-error">
320
- {message}
321
- </div>
322
- );
323
-
324
232
  export default SavedCardOption;