@akinon/pz-saved-card 1.59.0-rc.5 → 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,5 +1,25 @@
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
+
3
23
  ## 1.59.0-rc.5
4
24
 
5
25
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/pz-saved-card",
3
- "version": "1.59.0-rc.5",
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
+ );
@@ -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,28 +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
8
  import {
9
9
  useCompleteSavedCardMutation,
10
10
  useGetSavedCardsQuery,
11
11
  useSetSavedCardMutation
12
12
  } from '../redux/api';
13
- import { Button, Icon, Image } from '@akinon/next/components';
14
- import { getCreditCardType } from '../utils';
15
13
 
16
14
  import amex from '../../assets/img/amex.jpg';
17
15
  import mastercard from '../../assets/img/mastercard.png';
18
16
  import other from '../../assets/img/other.png';
19
17
  import troy from '../../assets/img/troy.png';
20
18
  import visa from '../../assets/img/visa.png';
21
-
22
- import SavedCardInstallments from '../components/installments';
23
- import {
24
- setCards,
25
- setDeletionModalId,
26
- setDeletionModalVisible
27
- } from '../redux/reducer';
19
+ import { setCards } from '../redux/reducer';
28
20
  import { DeleteConfirmationModal } from '../components/delete-confirmation-modal';
29
21
  import {
30
22
  AgreementAndSubmitProps,
@@ -33,6 +25,9 @@ import {
33
25
  InstallmentSectionProps,
34
26
  SavedCardOptionTexts
35
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';
36
31
 
37
32
  export const cardImages = { amex, mastercard, troy, visa, other };
38
33
 
@@ -107,10 +102,10 @@ const SavedCardOption = ({
107
102
  cardSelectionWrapperProps = {},
108
103
  installmentWrapperProps = {}
109
104
  }: SavedCardOptionProps) => {
110
- const mergedTexts = mergeTranslations(texts, defaultTranslations);
111
-
112
- const [selectedCard, setSelectedCard] = useState(null);
113
- const [formError, setFormError] = useState(null);
105
+ const mergedTexts = useMemo(
106
+ () => mergeTranslations(texts, defaultTranslations),
107
+ [texts]
108
+ );
114
109
  const dispatch = useAppDispatch();
115
110
  const { data: savedCards } = useGetSavedCardsQuery();
116
111
  const [completeSavedCard] = useCompleteSavedCardMutation();
@@ -124,21 +119,29 @@ const SavedCardOption = ({
124
119
  register,
125
120
  handleSubmit,
126
121
  control,
127
- formState: { errors }
122
+ formState: { errors },
123
+ setValue,
124
+ watch
128
125
  } = useForm({
129
126
  resolver: yupResolver(createFormSchema(mergedTexts.errors))
130
127
  });
131
128
 
129
+ const selectedCardToken = watch('card');
130
+ const selectedCard = useMemo(
131
+ () => cards?.find((card) => card.token === selectedCardToken),
132
+ [cards, selectedCardToken]
133
+ );
134
+
132
135
  const handleCardSelection = async (card) => {
133
136
  await setSavedCard({ card }).unwrap();
134
- setSelectedCard(card);
137
+ setValue('card', card.token, { shouldValidate: true });
135
138
  };
136
139
 
137
140
  const onSubmit = async () => {
138
141
  try {
139
142
  await completeSavedCard({ agreement: true });
140
143
  } catch (error) {
141
- setFormError(error);
144
+ console.error('Error completing saved card:', error);
142
145
  }
143
146
  };
144
147
 
@@ -146,7 +149,7 @@ const SavedCardOption = ({
146
149
  if (savedCards?.results && !cards?.length) {
147
150
  dispatch(setCards(savedCards.results));
148
151
  }
149
- }, [savedCards]);
152
+ }, [savedCards, cards, dispatch]);
150
153
 
151
154
  return (
152
155
  <>
@@ -209,7 +212,6 @@ const SavedCardOption = ({
209
212
  agreementCheckbox,
210
213
  control,
211
214
  errors,
212
- formError,
213
215
  buttonText: mergedTexts.button
214
216
  })
215
217
  ) : (
@@ -217,7 +219,6 @@ const SavedCardOption = ({
217
219
  agreementCheckbox={agreementCheckbox}
218
220
  control={control}
219
221
  errors={errors}
220
- formError={formError}
221
222
  buttonText={mergedTexts.button}
222
223
  />
223
224
  )}
@@ -228,118 +229,4 @@ const SavedCardOption = ({
228
229
  );
229
230
  };
230
231
 
231
- const CardSelectionSection = ({
232
- title,
233
- cards,
234
- selectedCard,
235
- onSelect,
236
- register,
237
- errors,
238
- dispatch
239
- }) => (
240
- <div className="border-solid border-gray-400 px-4 py-2">
241
- <span className="text-black-800 text-lg font-medium">{title}</span>
242
- <ul className="mt-4 text-xs w-full">
243
- {cards?.map((card) => (
244
- <li
245
- key={card.token}
246
- className="p-4 mb-2 border-2 border-gray-200 flex justify-between items-center cursor-pointer"
247
- onClick={() => onSelect(card)}
248
- >
249
- <input
250
- name="card"
251
- type="radio"
252
- checked={selectedCard?.token === card.token}
253
- value={card.token}
254
- id={card.token}
255
- className="mr-2"
256
- onChange={() => {}}
257
- {...register('card')}
258
- />
259
- <CardLabel card={card} />
260
- <DeleteIcon
261
- onClick={() => {
262
- dispatch(setDeletionModalId(card.id));
263
- dispatch(setDeletionModalVisible(true));
264
- }}
265
- />
266
- </li>
267
- ))}
268
- </ul>
269
- {errors.card && <ErrorText message={errors.card?.message} />}
270
- </div>
271
- );
272
-
273
- const CardLabel = ({ card }) => (
274
- <label className="flex flex-col w-full cursor-pointer md:flex-row md:items-center md:justify-between">
275
- <p className="w-full text-[10px] lg:w-1/3">{card.masked_card_number}</p>
276
- <Image
277
- className="w-8 h-6 object-contain flex items-center justify-center"
278
- width={50}
279
- height={50}
280
- src={cardImages[getCreditCardType(card.masked_card_number)].src}
281
- alt={card.name}
282
- />
283
- </label>
284
- );
285
-
286
- const DeleteIcon = ({ onClick }) => (
287
- <span className="cursor-pointer p-1" onClick={onClick}>
288
- <Icon name="close" size={12} />
289
- </span>
290
- );
291
-
292
- const InstallmentSection = ({
293
- title,
294
- selectedCard,
295
- installmentOptions,
296
- translations,
297
- errors
298
- }) => (
299
- <div className="border-solid border-gray-400 bg-white">
300
- <div className="px-4 py-2">
301
- <span className="text-black-800 text-lg font-medium">{title}</span>
302
- </div>
303
- <SavedCardInstallments
304
- selectedCard={selectedCard}
305
- installmentOptions={installmentOptions}
306
- translations={translations}
307
- error={errors}
308
- />
309
- </div>
310
- );
311
-
312
- const AgreementAndSubmit = ({
313
- agreementCheckbox,
314
- control,
315
- errors,
316
- formError,
317
- buttonText
318
- }) => (
319
- <div className="flex flex-col text-xs pb-4 px-4 sm:px-6">
320
- {agreementCheckbox &&
321
- cloneElement(agreementCheckbox, {
322
- control,
323
- error: errors.agreement,
324
- fieldId: 'agreement'
325
- })}
326
- {formError && (
327
- <ErrorText message={formError.non_field_errors ?? formError.status} />
328
- )}
329
- <Button
330
- type="submit"
331
- className="group uppercase mt-4 inline-flex items-center justify-center"
332
- >
333
- <span>{buttonText}</span>
334
- <Icon name="chevron-end" size={12} className="ml-2 h-3" />
335
- </Button>
336
- </div>
337
- );
338
-
339
- const ErrorText = ({ message }) => (
340
- <div className="w-full text-xs text-start px-1 mt-3 text-error">
341
- {message}
342
- </div>
343
- );
344
-
345
232
  export default SavedCardOption;