From 40a3ab09aa62b16ce940ce4e0e6e30360d9611fb Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Thu, 8 Aug 2019 10:47:38 +0300 Subject: [PATCH] Create SavedCardDetailsComponent --- .../SavedCardDetails/SavedCardDetails.css | 236 +++++++++++++++++ .../SavedCardDetails.example.js | 53 ++++ .../SavedCardDetails/SavedCardDetails.js | 240 ++++++++++++++++++ src/components/index.js | 1 + src/examples.js | 2 + src/translations/en.json | 10 + 6 files changed, 542 insertions(+) create mode 100644 src/components/SavedCardDetails/SavedCardDetails.css create mode 100644 src/components/SavedCardDetails/SavedCardDetails.example.js create mode 100644 src/components/SavedCardDetails/SavedCardDetails.js diff --git a/src/components/SavedCardDetails/SavedCardDetails.css b/src/components/SavedCardDetails/SavedCardDetails.css new file mode 100644 index 00000000..cc7e5899 --- /dev/null +++ b/src/components/SavedCardDetails/SavedCardDetails.css @@ -0,0 +1,236 @@ +@import '../../marketplace.css'; + +.root { +} + +.savedPaymentMethodTitle { + @apply --marketplaceH4FontStyles; + font-weight: bold; + cursor: pointer; +} + +.savedPaymentMethod { + display: inline-block; + margin-right: auto; +} + +.paymentMethodPlaceholderDesktop { + display: none; +} + +.paymentMethodPlaceholderMobile { + display: inline-block; +} + +@media (--viewportSmall) { + .paymentMethodPlaceholderDesktop { + display: inline-block; + } + + .paymentMethodPlaceholderMobile { + display: none; + } +} + +.cardIcon { + margin: 0 12px 0 4px; + width: 24px; + height: 16px; + display: inline-block; +} + +.closeIcon { + @apply --marketplaceModalCloseIcon; + margin-left: 0; + margin-right: 5px; +} + +.iconCheckmark { + stroke: var(--successColor); + display: inline-block; + margin: 12px 0 0 24px; +} + +.iconCheckmarkHidden { + visibility: hidden; + display: inline-block; + margin: 0 0 0 24px; +} + +.expirationDate { + padding-left: 24px; +} + +.expirationDateExpired { + padding-left: 24px; + color: var(--failColor); +} + +.cardExpiredText { + @apply --marketplaceH4FontStyles; + color: var(--failColor); + font-weight: var(--fontWeightSemiBold); + margin-top: 13px; + margin-bottom: 11px; +} + +.menu { + max-width: 100%; + height: auto; +} + +.menuLabel { + text-align: left; + font-weight: 500; + padding: 0; + border-radius: 4px; + border: 1px solid var(--matterColorNegative); + transition: all 0.2s ease-out; + z-index: calc(var(--zIndexPopup) + 1); + position: relative; + + &:hover { + background-color: var(--matterColorLight); + border: 1px solid var(--matterColorAnti); + } + + &:focus { + outline: none; + background-color: var(--matterColorLight); + border: 1px solid var(--matterColorNegative); + text-decoration: none; + border-radius: 4px 4px 0px 0px; + } +} + +.menuLabelWrapper, +.menuLabelWrapperExpired { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; + padding: 12px 24px 18px 16px; +} + +.menuLabelWrapperExpired { + border: 1px solid var(--failColor); +} + +.menuContent { + position: absolute; + z-index: var(--zIndexPopup); + + background-color: var(--matterColorLight); + box-shadow: var(--boxShadowFilterButton); + border-radius: 4px; + border: 1px solid var(--matterColorNegative); + + transition: all 0.15s ease-out; + + /* When the dropdown is open, we need to hack the box-shadow's position + to fit the whole dropdown menu component with padding-top + + MenuLabel height 56px + original padding-top 7px results to 63px + */ + padding: 63px 0 15px 0; + top: 0px; +} + +.menuItem { + color: var(--matterColor); + &:hover { + background-color: #f2f2f2; + } +} + +.menuText, +.menuTextReplaceCard { + @apply --marketplaceMessageFontStyles; + padding: 12px 8px; + color: var(--matterColor); + text-align: left; + font-weight: 500; + width: 80%; + + &:hover { + text-decoration: none; + } + + &:focus { + outline: 0; + background-color: #f2f2f2; + } +} + +.menuTextReplaceCard svg { + position: relative; + top: -2px; + width: 22px; + margin: 0 10px 0 4px; +} + +.menuDivider { + @apply --marketplaceH5FontStyles; + color: var(--matterColorAnti); + padding: 14px 24px 2px 24px; + margin: 0; +} + +.iconArrow { + stroke: var(--matterColor); + fill: var(--matterColor); + transition: all 0.2s ease-out; +} + +.IconArrowAnimation { + transform: rotate(180deg); +} + +.savedPaymentMethodDelete { + @apply --marketplaceH4FontStyles; + color: var(--matterColorAnti); + cursor: pointer; + margin-top: 14px; + margin-bottom: 10px; + + &:hover { + color: var(--matterColor); + } + + &:focus { + outline: none; + color: var(--matterColor); + } +} + +.modalButtonsWrapper { + padding-top: 80px; + display: flex; + justify-content: flex-end; + align-items: baseline; +} + +/* Title of the modal */ +.modalTitle { + @apply --marketplaceModalTitleStyles; + padding-top: 36px; +} + +/* Paragraph for the Modal */ +.modalMessage { + @apply --marketplaceModalParagraphStyles; + min-height: 200px; +} + +.cancelCardDelete { + color: var(--matterColorAnti); + cursor: pointer; + width: 50%; + text-align: right; + padding-right: 64px; + + &:hover { + color: var(--matterColor); + } +} diff --git a/src/components/SavedCardDetails/SavedCardDetails.example.js b/src/components/SavedCardDetails/SavedCardDetails.example.js new file mode 100644 index 00000000..150e5155 --- /dev/null +++ b/src/components/SavedCardDetails/SavedCardDetails.example.js @@ -0,0 +1,53 @@ +import { fakeIntl } from '../../util/test-data'; +import SavedCardDetails from './SavedCardDetails'; + +const noop = () => null; +const defaultProps = { + intl: fakeIntl, + onDeleteCard: noop, + onChange: noop, + onManageDisableScrolling: noop, +}; + +export const SavedCardDetailsExample = { + component: SavedCardDetails, + props: { + ...defaultProps, + card: { + brand: 'visa', + expirationMonth: 10, + expirationYear: 2050, + last4Digits: '3220', + }, + }, + group: 'payment methods', +}; + +export const SavedCardDetailsNoDelete = { + component: SavedCardDetails, + props: { + ...defaultProps, + card: { + brand: 'mastercard', + expirationMonth: 10, + expirationYear: 2050, + last4Digits: '3220', + }, + onDeleteCard: null, + }, + group: 'payment methods', +}; + +export const SavedCardDetailsExpired = { + component: SavedCardDetails, + props: { + ...defaultProps, + card: { + brand: 'amex', + expirationMonth: 7, + expirationYear: 2019, + last4Digits: '3220', + }, + }, + group: 'payment methods', +}; diff --git a/src/components/SavedCardDetails/SavedCardDetails.js b/src/components/SavedCardDetails/SavedCardDetails.js new file mode 100644 index 00000000..65efa14c --- /dev/null +++ b/src/components/SavedCardDetails/SavedCardDetails.js @@ -0,0 +1,240 @@ +import React, { useState } from 'react'; +import { bool, func, number, shape, string } from 'prop-types'; +import classNames from 'classnames'; +import { injectIntl, intlShape } from 'react-intl'; +import { + IconArrowHead, + IconCard, + IconClose, + IconCheckmark, + Button, + InlineTextButton, + Menu, + MenuLabel, + MenuItem, + MenuContent, + Modal, +} from '../../components'; +import css from './SavedCardDetails.css'; + +const DEFAULT_CARD = 'defaultCard'; +const REPLACE_CARD = 'replaceCard'; + +const SavedCardDetails = props => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); + const [active, setActive] = useState(DEFAULT_CARD); + + const { + rootClassName, + className, + intl, + card, + onChange, + onDeleteCard, + onManageDisableScrolling, + deletePaymentMethodInProgress, + } = props; + + const { last4Digits, expirationMonth, expirationYear, brand } = card || {}; + const classes = classNames(rootClassName || css.root, className); + + const paymentMethodPlaceholderDesktop = intl.formatMessage( + { id: 'SavedCardDetails.savedPaymentMethodPlaceholderDesktop' }, + { lastFour: last4Digits } + ); + + const paymentMethodPlaceholderMobile = intl.formatMessage( + { id: 'SavedCardDetails.savedPaymentMethodPlaceholderMobile' }, + { lastFour: last4Digits } + ); + + const paymentMethodPlaceholder = ( + <> + {paymentMethodPlaceholderDesktop} + {paymentMethodPlaceholderMobile} + + ); + + const replaceCardText = intl.formatMessage({ + id: 'SavedCardDetails.replaceCardText', + }); + const replaceCard = ( + + {replaceCardText} + + ); + + const expiredCardText = intl.formatMessage( + { id: 'SavedCardDetails.expiredCardText' }, + { lastFour: last4Digits } + ); + const expiredText =
{expiredCardText}
; + + const isExpired = (expirationMonth, expirationYear) => { + const currentTime = new Date(); + const currentYear = currentTime.getFullYear(); + const currentMonth = currentTime.getMonth() + 1; //getMonth() method returns the month (from 0 to 11) + + if (expirationYear < currentYear) { + return true; + } else if (expirationYear === currentYear && expirationMonth < currentMonth) { + return true; + } + + return false; + }; + + const isCardExpired = + expirationMonth && expirationYear && isExpired(expirationMonth, expirationYear); + + const defaultCard = ( +
+ + {paymentMethodPlaceholder} + + {expirationMonth}/{expirationYear.toString().substring(2)} + +
+ ); + + const handleClick = item => e => { + // Clicking buttons inside a form will call submit + e.preventDefault(); + e.stopPropagation(); + + setActive(item); + setMenuOpen(false); + if (onChange) { + onChange(item); + } + }; + + const onToggleActive = isOpen => { + setMenuOpen(isOpen); + }; + + const handleDeleteCard = () => { + setIsModalOpen(true); + }; + + const iconArrowClassName = menuOpen ? css.IconArrowAnimation : null; + + const replaceCardTitle = intl.formatMessage({ + id: 'SavedCardDetails.replaceCardTitle', + }); + const removeCardModalTitle = intl.formatMessage({ id: 'SavedCardDetails.removeCardModalTitle' }); + const removeCardModalContent = intl.formatMessage({ + id: 'SavedCardDetails.removeCardModalContent', + }); + const cancel = intl.formatMessage({ id: 'SavedCardDetails.cancel' }); + const removeCard = intl.formatMessage({ id: 'SavedCardDetails.removeCard' }); + const deletePaymentMethod = intl.formatMessage({ id: 'SavedCardDetails.deletePaymentMethod' }); + + const showExpired = isCardExpired && active === DEFAULT_CARD; + + return ( +
+ + +
+ {active === DEFAULT_CARD ? defaultCard : replaceCard} + + + +
+
+ + + + + + {defaultCard} + + + + {replaceCardTitle} + + + + + {replaceCard} + + + +
+ {showExpired && !menuOpen ? expiredText : null} + + {onDeleteCard && active !== REPLACE_CARD ? ( + + + {deletePaymentMethod} + + ) : null} + + { + setIsModalOpen(false); + }} + contentClassName={css.modalContent} + onManageDisableScrolling={onManageDisableScrolling} + > +
+
{removeCardModalTitle}
+

{removeCardModalContent}

+
+
setIsModalOpen(false)} className={css.cancelCardDelete}> + {cancel} +
+ +
+
+
+
+ ); +}; + +SavedCardDetails.defaultProps = { + rootClassName: null, + className: null, + card: null, + onChange: null, + onDeleteCard: null, + deletePaymentMethodInProgress: false, +}; + +SavedCardDetails.propTypes = { + rootClassName: string, + className: string, + intl: intlShape.isRequired, + card: shape({ + brand: string.isRequired, + expirationMonth: number.isRequired, + expirationYear: number.isRequired, + last4Digits: string.isRequired, + }), + onChange: func, + onDeleteCard: func, + onManageDisableScrolling: func.isRequired, + deletePaymentMethodInProgress: bool, +}; + +export default injectIntl(SavedCardDetails); diff --git a/src/components/index.js b/src/components/index.js index 5aa72405..ba9a7c7e 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -128,6 +128,7 @@ export { default as OrderDiscussionPanel } from './OrderDiscussionPanel/OrderDis export { default as Page } from './Page/Page'; export { default as PriceFilter } from './PriceFilter/PriceFilter'; export { default as Reviews } from './Reviews/Reviews'; +export { default as SavedCardDetails } from './SavedCardDetails/SavedCardDetails'; export { default as SearchFilters } from './SearchFilters/SearchFilters'; export { default as SearchFiltersMobile } from './SearchFiltersMobile/SearchFiltersMobile'; export { default as SearchFiltersPanel } from './SearchFiltersPanel/SearchFiltersPanel'; diff --git a/src/examples.js b/src/examples.js index fbc7b6cb..5af40b8f 100644 --- a/src/examples.js +++ b/src/examples.js @@ -59,6 +59,7 @@ import * as RangeSlider from './components/RangeSlider/RangeSlider.example'; import * as ResponsiveImage from './components/ResponsiveImage/ResponsiveImage.example'; import * as ReviewRating from './components/ReviewRating/ReviewRating.example'; import * as Reviews from './components/Reviews/Reviews.example'; +import * as SavedCardDetails from './components/SavedCardDetails/SavedCardDetails.example'; import * as SectionThumbnailLinks from './components/SectionThumbnailLinks/SectionThumbnailLinks.example'; import * as SelectMultipleFilter from './components/SelectMultipleFilter/SelectMultipleFilter.example'; import * as StripeBankAccountTokenInputField from './components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example'; @@ -171,6 +172,7 @@ export { ReviewForm, ReviewRating, Reviews, + SavedCardDetails, SectionThumbnailLinks, SelectMultipleFilter, SendMessageForm, diff --git a/src/translations/en.json b/src/translations/en.json index 941e8eb7..7628f21c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -684,6 +684,16 @@ "ReviewModal.description": "Reviews are an important part of the Saunatime community. Please share what went well and what could have been improved.", "ReviewModal.later": "Later", "ReviewModal.title": "Leave a review for {revieweeName}", + "SavedCardDetails.cancel": "Cancel", + "SavedCardDetails.deletePaymentMethod": "Delete payment card", + "SavedCardDetails.expiredCardText": "This payment card has expired", + "SavedCardDetails.removeCard": "Remove card", + "SavedCardDetails.removeCardModalContent": "The card will be removed from your account and can't be used for future purchases.", + "SavedCardDetails.removeCardModalTitle": "Are you sure you want to remove this credit card?", + "SavedCardDetails.replaceCardText": "Credit or debit card", + "SavedCardDetails.replaceCardTitle": "Use another payment method", + "SavedCardDetails.savedPaymentMethodPlaceholderDesktop": "•••• •••• •••• {lastFour}", + "SavedCardDetails.savedPaymentMethodPlaceholderMobile": "…{lastFour}", "SearchFilters.amenitiesLabel": "Amenities", "SearchFilters.categoryLabel": "Category", "SearchFilters.filtersButtonLabel": "Filters",