diff --git a/packages/peregrine/lib/Apollo/policies/__tests__/__snapshots__/index.spec.js.snap b/packages/peregrine/lib/Apollo/policies/__tests__/__snapshots__/index.spec.js.snap index fd59da86ce..679b5332a3 100644 --- a/packages/peregrine/lib/Apollo/policies/__tests__/__snapshots__/index.spec.js.snap +++ b/packages/peregrine/lib/Apollo/policies/__tests__/__snapshots__/index.spec.js.snap @@ -66,6 +66,9 @@ Object { }, }, }, + "CustomerPaymentTokens": Object { + "keyFields": [Function], + }, "ProductImage": Object { "keyFields": Array [ "url", diff --git a/packages/peregrine/lib/Apollo/policies/index.js b/packages/peregrine/lib/Apollo/policies/index.js index 5f5669b6c7..32f8c10dc3 100644 --- a/packages/peregrine/lib/Apollo/policies/index.js +++ b/packages/peregrine/lib/Apollo/policies/index.js @@ -143,6 +143,9 @@ const typePolicies = { } } }, + CustomerPaymentTokens: { + keyFields: () => 'CustomerPaymentTokens' + }, ProductImage: { keyFields: ['url'] }, diff --git a/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/__snapshots__/useSavedPaymentsPage.spec.js.snap b/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/__snapshots__/useSavedPaymentsPage.spec.js.snap new file mode 100644 index 0000000000..4e4bb1f389 --- /dev/null +++ b/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/__snapshots__/useSavedPaymentsPage.spec.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it returns the proper shape 1`] = ` +Object { + "isLoading": false, + "savedPayments": Array [], +} +`; diff --git a/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/useSavedPaymentsPage.spec.js b/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/useSavedPaymentsPage.spec.js index 09c3de03bb..b8e7b5ae54 100644 --- a/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/useSavedPaymentsPage.spec.js +++ b/packages/peregrine/lib/talons/SavedPaymentsPage/__tests__/useSavedPaymentsPage.spec.js @@ -69,7 +69,7 @@ const Component = props => { }; const props = { - queries: { + operations: { getSavedPaymentsQuery: 'getSavedPaymentsQuery' } }; @@ -80,9 +80,8 @@ test('it returns the proper shape', () => { // Assert. const talonProps = log.mock.calls[0][0]; - const actualKeys = Object.keys(talonProps); - const expectedKeys = ['savedPayments', 'handleAddPayment', 'isLoading']; - expect(actualKeys.sort()).toEqual(expectedKeys.sort()); + + expect(talonProps).toMatchSnapshot(); }); test('it returns the savedPayments correctly when present', () => { diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.gql.js b/packages/peregrine/lib/talons/SavedPaymentsPage/savedPaymentsPage.gql.js similarity index 77% rename from packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.gql.js rename to packages/peregrine/lib/talons/SavedPaymentsPage/savedPaymentsPage.gql.js index 1ef294a254..a7b2411570 100644 --- a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.gql.js +++ b/packages/peregrine/lib/talons/SavedPaymentsPage/savedPaymentsPage.gql.js @@ -1,7 +1,7 @@ import { gql } from '@apollo/client'; export const GET_SAVED_PAYMENTS_QUERY = gql` - query getSavedPayments { + query GetSavedPayments { customerPaymentTokens { items { details @@ -13,7 +13,5 @@ export const GET_SAVED_PAYMENTS_QUERY = gql` `; export default { - queries: { - GET_SAVED_PAYMENTS_QUERY - } + getSavedPaymentsQuery: GET_SAVED_PAYMENTS_QUERY }; diff --git a/packages/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage.js b/packages/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage.js index b9d1aebe96..8552125597 100644 --- a/packages/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage.js +++ b/packages/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage.js @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { useQuery } from '@apollo/client'; @@ -6,6 +6,9 @@ import { useQuery } from '@apollo/client'; import { useAppContext } from '@magento/peregrine/lib/context/app'; import { useUserContext } from '@magento/peregrine/lib/context/user'; +import mergeOperations from '@magento/peregrine/lib/util/shallowMerge'; +import defaultOperations from './savedPaymentsPage.gql'; + export const normalizeTokens = responseData => { const paymentTokens = (responseData && responseData.customerPaymentTokens.items) || []; @@ -26,17 +29,16 @@ export const normalizeTokens = responseData => { * @function * * @param {Object} props - * @param {SavedPaymentsPageQueries} props.queries GraphQL queries + * @param {SavedPaymentsPageQueries} props.operations GraphQL queries * * @returns {SavedPaymentsPageTalonProps} * * @example Importing into your project * import { useSavedPayments } from '@magento/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage'; */ -export const useSavedPaymentsPage = props => { - const { - queries: { getSavedPaymentsQuery } - } = props; +export const useSavedPaymentsPage = (props = {}) => { + const operations = mergeOperations(defaultOperations, props.operations); + const { getSavedPaymentsQuery } = operations; const [ , @@ -68,14 +70,9 @@ export const useSavedPaymentsPage = props => { setPageLoading(loading); }, [loading, setPageLoading]); - const handleAddPayment = useCallback(() => { - // TODO in PWA-637 - }, []); - const savedPayments = normalizeTokens(savedPaymentsData); return { - handleAddPayment, isLoading: loading, savedPayments }; @@ -90,7 +87,7 @@ export const useSavedPaymentsPage = props => { * * @property {GraphQLAST} getSavedPaymentsQuery Query for getting saved payments. See https://devdocs.magento.com/guides/v2.4/graphql/queries/customer-payment-tokens.html * - * @see [savedPaymentsPage.gql.js]{@link https://github.com/magento/pwa-studio/blob/develop/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.gql.js} + * @see [savedPaymentsPage.gql.js]{@link https://github.com/magento/pwa-studio/blob/develop/packages/peregrine/lib/talons/SavedPaymentsPage/savedPaymentsPage.gql.js} * for queries used in Venia */ diff --git a/packages/venia-ui/i18n/en_US.json b/packages/venia-ui/i18n/en_US.json index c782dbeb7e..cc86197d28 100644 --- a/packages/venia-ui/i18n/en_US.json +++ b/packages/venia-ui/i18n/en_US.json @@ -51,8 +51,8 @@ "categoryLeaf.allLabel": "All {name}", "categoryList.errorFetch": "Data Fetch Error: ", "categoryList.noResults": "No child categories found.", - "checkoutPage.additionalText": "You will also receive an email with the details and we will let you know when your order has shipped.", "checkoutPage.accountSuccessfullyCreated": "Account successfully created.", + "checkoutPage.additionalText": "You will also receive an email with the details and we will let you know when your order has shipped.", "checkoutPage.billingAddressSame": "Billing address same as shipping address", "checkoutPage.checkout": "Checkout", "checkoutPage.couponCode": "Enter Coupon Code", @@ -84,8 +84,8 @@ "checkoutPage.quantity": "Qty : {quantity}", "checkoutPage.quickCheckout": "Quick Checkout When You Return", "checkoutPage.returnToCart": "Return to Cart", - "checkoutPage.reviewOrder": "Review Order", "checkoutPage.reviewAndPlaceOrder": "Review and Place Order", + "checkoutPage.reviewOrder": "Review Order", "checkoutPage.setAPasswordAndSave": "Set a password and save your information for next time in one easy step!", "checkoutPage.shippingMethodStep": "2. Shipping Method", "checkoutPage.showAllItems": "SHOW ALL ITEMS", @@ -206,8 +206,8 @@ "Live Chat": "Live Chat", "loadingIndicator.message": "Fetching Data...", "logo.title": "Venia", - "magentoRoute.routeError": "That page could not be found. Please try again.", "magentoRoute.internalError": "Something went wrong. Please try again.", + "magentoRoute.routeError": "That page could not be found. Please try again.", "miniCart.checkout": "CHECKOUT", "miniCart.editCartButton": "Edit Shopping Bag", "miniCart.emptyMessage": "There are no items in your cart.", @@ -228,6 +228,7 @@ "orderDetails.billingInformationLabel": "Billing Information", "orderDetails.buyAgain": "Buy Again", "orderDetails.discount": "Discount", + "orderDetails.noShippingInformation": "No shipping information", "orderDetails.orderTotal": "Order Total", "orderDetails.paymentMethodLabel": "Payment Method", "orderDetails.printLabel": "Print Receipt", @@ -240,8 +241,10 @@ "orderDetails.tax": "Tax", "orderDetails.total": "Total", "orderDetails.trackingInformation": "Tracking number: {number}", + "orderDetails.waitingOnTracking": "Waiting for tracking information", "orderHistoryPage.emptyDataMessage": "You don't have any orders yet.", "orderHistoryPage.pageTitleText": "Order History", + "orderItems.itemsHeading": "Items", "orderProgressBar.deliveredText": "Delivered", "orderProgressBar.processingText": "Processing", "orderProgressBar.readyToShipText": "Ready to ship", @@ -255,9 +258,10 @@ "orderRow.shippedText": "Shipped", "Our Story": "Our Story", "pagination.firstPage": "move to the first page", - "pagination.prevPage": "move to the previous page", - "pagination.nextPage": "move to the next page", "pagination.lastPage": "move to the last page", + "pagination.nextPage": "move to the next page", + "pagination.prevPage": "move to the previous page", + "postcode.label": "ZIP / Postal Code", "priceAdjustments.couponCode": "Enter Coupon Code", "priceAdjustments.giftOptions": "See Gift Options", "priceAdjustments.shippingMethod": "Estimate your Shipping", @@ -295,7 +299,6 @@ "productOptions.selectedLabel": "Selected {label}:", "productQuantity.label": "product's quantity", "productSort.sortButton": "Sort", - "postcode.label": "ZIP / Postal Code", "quantity.buttonDecrement": "Decrease Quantity", "quantity.buttonIncrement": "Increase Quantity", "quantity.input": "Item Quantity", @@ -305,13 +308,13 @@ "resetPassword.invalidTokenMessage": "Uh oh, something went wrong. Check the link or try again.", "resetPassword.newPasswordText": "New Password", "resetPassword.pageTitleText": "Reset Password", - "resetPassword.savePassword": "Save Password", "resetPassword.savedPasswordText": "Your new password has been saved.", + "resetPassword.savePassword": "Save Password", "resetPassword.successMessage": "Your new password has been saved. Please use this password to sign into your Account.", "Returns": "Returns", "savedPaymentsPage.addButtonText": "Add a credit card", - "savedPaymentsPage.subHeading": "Credit Cards saved here will be available during checkout.", - "savedPaymentsPage.title": "Saved Payments - {store_name}", + "savedPaymentsPage.noSavedPayments": "You have no saved payments.", + "savedPaymentsPage.title": "Saved Payments", "searchBar.heading": "Product Suggestions", "searchBar.label": " in {label}", "searchPage.filterButton": "Filter", @@ -330,14 +333,14 @@ "shippingInformation.editTitle": "1. Shipping Information", "shippingInformation.loading": "Fetching Shipping Information...", "shippingMethod.continueToNextStep": "Continue to Payment Information", - "shippingMethod.loading": "Loading shipping methods...", "shippingMethod.heading": "Shipping Method", + "shippingMethod.loading": "Loading shipping methods...", "shippingMethods.estimateButton": "I want to estimate my shipping", "shippingMethods.message": "For shipping estimates before proceeding to checkout, please provide the Country, State, and ZIP for the destination of your order.", "shippingMethods.prompt": "Shipping Methods", + "shippingRadios.errorLoading": "Error loading shipping methods. Please ensure a shipping address is set and try again.", "shippingSummary.estimatedShipping": "Estimated Shipping", "shippingSummary.shipping": "Shipping", - "shippingRadios.errorLoading": "Error loading shipping methods. Please ensure a shipping address is set and try again.", "Sign In": "Sign In", "signIn.createAccountText": "Create an Account", "signIn.emailAddressText": "Email address", @@ -350,6 +353,8 @@ "sortItem.priceDesc": "Price: High to Low", "sortItem.relevance": "Best Match", "stockStatusMessage.message": "An item in your cart is currently out-of-stock and must be removed in order to Checkout.", + "storedPayments.creditCard": "Credit Card", + "storedPayments.delete": "Delete", "taxSummary.estimatedTax": "Estimated Tax", "taxSummary.tax": "Tax", "validation.hasLengthAtLeast": "Must contain at least {value} character(s).", @@ -366,21 +371,18 @@ "wishlist.emptyListText": "There are currently no items in this list", "wishlist.privateText": "Private", "wishlist.publicText": "Public", + "wishlistConfirmRemoveProductDialog.confirmationPrompt": "Are you sure you want to delete this product from the list?", + "wishlistConfirmRemoveProductDialog.confirmButton": "Delete", + "wishlistConfirmRemoveProductDialog.errorMessage": "There was an error deleting this product. Please try again later.", + "wishlistConfirmRemoveProductDialog.title": "Remove Product from Wishlist", "wishlistItem.addToCart": "Add to Cart", "wishlistItem.addToCartError": "Something went wrong. Please refresh and try again.", "wishlistMoreActionsDialog.copy": "Copy to", "wishlistMoreActionsDialog.delete": "Remove", "wishlistMoreActionsDialog.move": "Move to", "wishlistMoreActionsDialog.title": "Actions", - "wishlistConfirmRemoveProductDialog.confirmButton": "Delete", - "wishlistConfirmRemoveProductDialog.confirmationPrompt": "Are you sure you want to delete this product from the list?", - "wishlistConfirmRemoveProductDialog.errorMessage": "There was an error deleting this product. Please try again later.", - "wishlistConfirmRemoveProductDialog.title": "Remove Product from Wishlist", "wishlistPage.disabledMessage": "Sorry, this feature has been disabled.", "wishlistPage.fetchErrorMessage": "Something went wrong. Please refresh and try again.", "wishlistPage.headingText": "Favorites Lists", - "wishlistPage.wishlistDisabledMessage": "The wishlist is not currently available.", - "orderDetails.waitingOnTracking": "Waiting for tracking information", - "orderItems.itemsHeading": "Items", - "orderDetails.noShippingInformation": "No shipping information" + "wishlistPage.wishlistDisabledMessage": "The wishlist is not currently available." } diff --git a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethodCollection.js b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethodCollection.js index ff47c24c3a..6250110945 100644 --- a/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethodCollection.js +++ b/packages/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethodCollection.js @@ -1,6 +1,7 @@ /** * This file is augmented at build time using the @magento/venia-ui build - * target "payments", which allows third-party modules to add new payment component mappings. + * target "checkoutPagePaymentTypes", which allows third-party modules to + * add new payment component mappings for the checkout page. * * @see [Payment definition object]{@link PaymentDefinition} */ @@ -17,6 +18,6 @@ export default {}; * @example A custom payment method * const myCustomPayment = { * paymentCode: 'cc', - * importPath: '@partner/module/path_to_your_component' + * importPath: '@partner/module/path_to_your_component' * } */ diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/creditCard.spec.js.snap b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/creditCard.spec.js.snap new file mode 100644 index 0000000000..f14720c4df --- /dev/null +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/creditCard.spec.js.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should render properly 1`] = ` +
+
+ +
+
+ **** 1234    Visa +
+
+ Dec. 12 +
+
+ +
+
+`; diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/paymentCard.spec.js.snap b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/paymentCard.spec.js.snap new file mode 100644 index 0000000000..a274d240bc --- /dev/null +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/paymentCard.spec.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should render properly 1`] = ` + +`; diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/savedPaymentsPage.spec.js.snap b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/savedPaymentsPage.spec.js.snap index 002b0ac498..33fa304cf5 100644 --- a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/savedPaymentsPage.spec.js.snap +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/__snapshots__/savedPaymentsPage.spec.js.snap @@ -8,54 +8,26 @@ exports[`renders correctly when there are existing saved payments 1`] = `

- Saved Payments - Venia + Saved Payments

-

- Credit Cards saved here will be available during checkout. -

- -
-
-
-
-
-
-
-
-
-
-
-
+
+
`; @@ -67,41 +39,95 @@ exports[`renders correctly when there are no existing saved payments 1`] = `

- Saved Payments - Venia + Saved Payments

-

- Credit Cards saved here will be available during checkout. -

+
- + You have no saved payments.
`; + +exports[`renders loading indicator when isLoading is true 1`] = ` +
+ + + + + + + + + + + + + + + +
+`; diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/creditCard.spec.js b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/creditCard.spec.js new file mode 100644 index 0000000000..890ca23145 --- /dev/null +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/creditCard.spec.js @@ -0,0 +1,18 @@ +import React from 'react'; +import CreditCard from '../creditCard'; + +import { createTestInstance } from '@magento/peregrine'; + +const props = { + details: { + maskedCC: '1234', + type: 'VI', + expirationDate: '12/12/2022' + } +}; + +test('Should render properly', () => { + const instance = createTestInstance(); + + expect(instance.toJSON()).toMatchSnapshot(); +}); diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/paymentCard.spec.js b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/paymentCard.spec.js new file mode 100644 index 0000000000..10773e5229 --- /dev/null +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/paymentCard.spec.js @@ -0,0 +1,23 @@ +import React from 'react'; +import PaymentCard from '../paymentCard'; + +import { createTestInstance } from '@magento/peregrine'; + +jest.mock('../savedPaymentTypes', () => ({ + braintree: props => +})); + +const props = { + payment_method_code: 'braintree', + details: { + maskedCC: '1234', + type: 'VI', + expirationDate: '12/12/2022' + } +}; + +test('Should render properly', () => { + const instance = createTestInstance(); + + expect(instance.toJSON()).toMatchSnapshot(); +}); diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/savedPaymentsPage.spec.js b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/savedPaymentsPage.spec.js index 435e5aa7cf..dff14ad0a9 100644 --- a/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/savedPaymentsPage.spec.js +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/__tests__/savedPaymentsPage.spec.js @@ -7,7 +7,6 @@ import SavedPaymentsPage from '../savedPaymentsPage'; jest.mock('@magento/venia-ui/lib/classify'); jest.mock('../../Head', () => ({ Title: () => 'Title' })); -jest.mock('../../Icon', () => 'Icon'); jest.mock( '@magento/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage', () => { @@ -16,14 +15,35 @@ jest.mock( }; } ); +jest.mock('../paymentCard', () => props => ); const props = {}; const talonProps = { - savedPayments: [], - handleAddPayment: jest.fn().mockName('handleAddPayment') + savedPayments: [ + { + public_hash: '78asfg87ibafv', + payment_method_code: 'braintree', + details: { + maskedCC: '1234', + type: 'VI', + expirationDate: '12/12/2022' + } + } + ] }; -it('renders correctly when there are no existing saved payments', () => { +test('renders correctly when there are no existing saved payments', () => { + // Arrange. + useSavedPaymentsPage.mockReturnValueOnce({ savedPayments: [] }); + + // Act. + const instance = createTestInstance(); + + // Assert. + expect(instance.toJSON()).toMatchSnapshot(); +}); + +test('renders correctly when there are existing saved payments', () => { // Arrange. useSavedPaymentsPage.mockReturnValueOnce(talonProps); @@ -34,13 +54,12 @@ it('renders correctly when there are no existing saved payments', () => { expect(instance.toJSON()).toMatchSnapshot(); }); -it('renders correctly when there are existing saved payments', () => { +test('renders loading indicator when isLoading is true', () => { // Arrange. - const myTalonProps = { + useSavedPaymentsPage.mockReturnValueOnce({ ...talonProps, - savedPayments: ['a', 'b', 'c'] - }; - useSavedPaymentsPage.mockReturnValueOnce(myTalonProps); + isLoading: true + }); // Act. const instance = createTestInstance(); diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.css b/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.css new file mode 100644 index 0000000000..bf8fb00200 --- /dev/null +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.css @@ -0,0 +1,48 @@ +.root { + border-radius: 0.375rem; + border: 2px solid rgb(var(--venia-global-color-gray-400)); + column-gap: 1rem; + display: grid; + grid-template-columns: 1fr auto; + min-height: 10rem; + min-width: 20rem; + padding: 1.5rem 2rem; +} + +.root_selected { + composes: root; + border-color: rgb(var(--venia-brand-color-1-600)); +} + +.title { + font-weight: var(--venia-global-fontWeight-semibold); + grid-column: 1 / span 1; + grid-row: 1 / span 1; +} + +.number { + grid-column: 1 / span 1; + grid-row: 2 / span 1; +} + +.expiry_date { + grid-column: 1 / span 1; + grid-row: 3 / span 1; +} + +.delete { + grid-column: 2 / span 1; + grid-row: 1 / span 3; +} + +.deleteButton { + composes: root from '../LinkButton/linkButton.css'; + text-decoration: none; + visibility: hidden; +} + +@media (max-width: 960px) { + .deleteText { + display: none; + } +} diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.js b/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.js new file mode 100644 index 0000000000..c8fd5f013b --- /dev/null +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/creditCard.js @@ -0,0 +1,98 @@ +import React, { useCallback, useMemo } from 'react'; +import { shape, string } from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Trash2 as DeleteIcon } from 'react-feather'; + +import LinkButton from '../LinkButton'; +import Icon from '../Icon'; +import { mergeClasses } from '@magento/venia-ui/lib/classify'; + +import defaultClasses from './creditCard.css'; + +/** + * Enumerated list of supported credit card types from + * + * https://github.com/magento/magento2/blob/2.4-develop/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js + */ +const cardTypeMapper = { + AE: 'American Express', + AU: 'Aura', + DI: 'Discover', + DN: 'Diners', + ELO: 'Elo', + HC: 'Hipercard', + JCB: 'JCB', + MC: 'MasterCard', + MD: 'Maestro Domestic', + MI: 'Maestro International', + UN: 'UnionPay', + VI: 'Visa' +}; + +const CreditCard = props => { + const { classes: propClasses, details } = props; + const classes = mergeClasses(defaultClasses, propClasses); + + const number = `**** ${details.maskedCC} \u00A0\u00A0 ${cardTypeMapper[ + details.type + ] || ''}`; + const cardExpiryDate = useMemo(() => { + const [month, year] = details.expirationDate.split('/'); + const shortMonth = new Date(+year, +month - 1).toLocaleString( + 'default', + { month: 'short' } + ); + + return `${shortMonth}. ${year}`; + }, [details.expirationDate]); + + // Should be moved to a talon in the future + const handleDelete = useCallback(() => {}, []); + const deleteButton = ( + + + + + + + ); + + return ( +
+
+ +
+
{number}
+
{cardExpiryDate}
+
{deleteButton}
+
+ ); +}; + +export default CreditCard; + +CreditCard.propTypes = { + classes: shape({ + delete: 'string', + deleteButton: 'string', + expiry_date: 'string', + number: 'string', + root_selected: 'string', + root: 'string', + title: 'string' + }), + details: shape({ + expirationDate: string, + maskedCC: string, + type: string + }) +}; diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/paymentCard.js b/packages/venia-ui/lib/components/SavedPaymentsPage/paymentCard.js new file mode 100644 index 0000000000..cb38d1ddd0 --- /dev/null +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/paymentCard.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { shape, string } from 'prop-types'; + +import paymentCardMapper from './savedPaymentTypes'; + +const PaymentCard = props => { + const PaymentComponent = paymentCardMapper[props.payment_method_code]; + + if (!PaymentComponent) { + /** + * Will be handled in https://jira.corp.magento.com/browse/PWA-1202 + */ + } + + return ; +}; + +export default PaymentCard; + +PaymentCard.propTypes = { + details: shape({ + expirationDate: string, + maskedCC: string, + type: string + }), + payment_method_code: string.isRequired +}; diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentTypes.js b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentTypes.js new file mode 100644 index 0000000000..b8d34b1e6b --- /dev/null +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentTypes.js @@ -0,0 +1,22 @@ +/** + * This file is augmented at build time using the @magento/venia-ui build + * target "savedPaymentTypes", which allows third-party modules to add new saved payment component mappings. + * + * @see [SavedPayment definition object]{@link SavedPaymentDefinition} + */ +export default {}; + +/** + * A payment definition object that describes a saved payment in your storefront. + * + * @typedef {Object} SavedPaymentDefinition + * @property {string} paymentCode is use to map your payment + * @property {string} importPath Resolvable path to the component the + * Route component will render + * + * @example A custom payment method + * const myCustomSavedPayment = { + * paymentCode: 'cc', + * importPath: '@partner/module/path_to_your_component' + * } + */ diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.css b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.css index fb952d812a..2fe729d5d1 100644 --- a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.css +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.css @@ -25,6 +25,10 @@ grid-template-columns: 1fr 1fr 1fr; } +.noPayments { + text-align: center; +} + .addButton { border: 2px dashed rgb(var(--venia-global-color-gray-400)); border-radius: 0.375rem; diff --git a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.js b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.js index 8355c4af67..9e3b8a989a 100644 --- a/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.js +++ b/packages/venia-ui/lib/components/SavedPaymentsPage/savedPaymentsPage.js @@ -1,72 +1,63 @@ import React, { useMemo } from 'react'; import { useIntl } from 'react-intl'; -import { PlusSquare } from 'react-feather'; import { useSavedPaymentsPage } from '@magento/peregrine/lib/talons/SavedPaymentsPage/useSavedPaymentsPage'; import { mergeClasses } from '@magento/venia-ui/lib/classify'; -import Icon from '../Icon'; -import LinkButton from '../LinkButton'; import { Title } from '../Head'; +import PaymentCard from './paymentCard'; +import { fullPageLoadingIndicator } from '../LoadingIndicator'; -import { GET_SAVED_PAYMENTS_QUERY } from './savedPaymentsPage.gql'; import defaultClasses from './savedPaymentsPage.css'; const SavedPaymentsPage = props => { - const talonProps = useSavedPaymentsPage({ - queries: { - getSavedPaymentsQuery: GET_SAVED_PAYMENTS_QUERY - } - }); + const talonProps = useSavedPaymentsPage(); - const { handleAddPayment, savedPayments } = talonProps; + const { isLoading, savedPayments } = talonProps; const classes = mergeClasses(defaultClasses, props.classes); + const { formatMessage } = useIntl(); + const savedPaymentElements = useMemo(() => { - return savedPayments.map( - ({ details, public_hash, payment_method_code }) => ( - // TODO: Clean up in PWA-636 -
-
{payment_method_code}
-
{JSON.stringify(details, null, 2)}
-
- ) - ); + if (savedPayments.length) { + return savedPayments.map(paymentDetails => ( + + )); + } else { + return null; + } }, [savedPayments]); - const { formatMessage } = useIntl(); + const noSavedPayments = useMemo(() => { + if (!savedPayments.length) { + return formatMessage({ + id: 'savedPaymentsPage.noSavedPayments', + defaultMessage: 'You have no saved payments.' + }); + } else { + return null; + } + }, [savedPayments, formatMessage]); - // STORE_NAME is injected by Webpack at build time. - const title = formatMessage( - { id: 'savedPaymentsPage.title' }, - { store_name: STORE_NAME } - ); - const subHeading = formatMessage({ id: 'savedPaymentsPage.subHeading' }); - const addButtonText = formatMessage({ - id: 'savedPaymentsPage.addButtonText' + const title = formatMessage({ + id: 'savedPaymentsPage.title', + defaultMessage: 'Saved Payments' }); + if (isLoading) { + return fullPageLoadingIndicator; + } + return (
{title}

{title}

-

{subHeading}

-
- - - {addButtonText} - - {savedPaymentElements} -
+
{savedPaymentElements}
+
{noSavedPayments}
); }; diff --git a/packages/venia-ui/lib/targets/PaymentMethodList.js b/packages/venia-ui/lib/targets/CheckoutPagePaymentsList.js similarity index 75% rename from packages/venia-ui/lib/targets/PaymentMethodList.js rename to packages/venia-ui/lib/targets/CheckoutPagePaymentsList.js index 1146ccbd83..166c01a1b4 100644 --- a/packages/venia-ui/lib/targets/PaymentMethodList.js +++ b/packages/venia-ui/lib/targets/CheckoutPagePaymentsList.js @@ -1,14 +1,14 @@ /** - * Implementation of our 'payments' target. This will gather + * Implementation of our 'checkoutPagePaymentTypes' target. This will gather * PaymentMethod declarations { paymentCode, importPath } from all * interceptors, and then tap `builtins.transformModules` to inject a module * transform into the build which is configured to generate an object of modules * to be imported and then exported. * * An instance of this class is made available when you use VeniaUI's - * `payments` target. + * `checkoutPagePaymentTypes` target. */ -class PaymentMethodList { +class CheckoutPagePaymentsList { /** @hideconstructor */ constructor(venia) { const registry = this; @@ -16,7 +16,7 @@ class PaymentMethodList { module: '@magento/venia-ui/lib/components/CheckoutPage/PaymentInformation/paymentMethodCollection.js', publish(targets) { - targets.payments.call(registry); + targets.checkoutPagePaymentTypes.call(registry); } }); } @@ -26,4 +26,4 @@ class PaymentMethodList { } } -module.exports = PaymentMethodList; +module.exports = CheckoutPagePaymentsList; diff --git a/packages/venia-ui/lib/targets/SavedPaymentTypes.js b/packages/venia-ui/lib/targets/SavedPaymentTypes.js new file mode 100644 index 0000000000..7631e012ab --- /dev/null +++ b/packages/venia-ui/lib/targets/SavedPaymentTypes.js @@ -0,0 +1,32 @@ +/** + * Implementation of our 'savedPaymentTypes' target. This will gather + * SavedPaymentMethod declarations { paymentCode, importPath } from all + * interceptors, and then tap `builtins.transformModules` to inject a module + * transform into the build which is configured to generate an object of modules + * to be imported and then exported. + * + * An instance of this class is made available when you use VeniaUI's + * `savedPaymentTypes` target. + * + * The SavedPaymentMethod declarations collected as part of this target will be + * used to render the saved payment methods section in My Account. + */ +class SavedPaymentTypes { + /** @hideconstructor */ + constructor(venia) { + const registry = this; + this._methods = venia.esModuleObject({ + module: + '@magento/venia-ui/lib/components/SavedPaymentsPage/savedPaymentTypes.js', + publish(targets) { + targets.savedPaymentTypes.call(registry); + } + }); + } + + add({ paymentCode, importPath }) { + this._methods.add(`import ${paymentCode} from '${importPath}'`); + } +} + +module.exports = SavedPaymentTypes; diff --git a/packages/venia-ui/lib/targets/__tests__/SavedPaymentTypes.spec.js b/packages/venia-ui/lib/targets/__tests__/SavedPaymentTypes.spec.js new file mode 100644 index 0000000000..79370c54c5 --- /dev/null +++ b/packages/venia-ui/lib/targets/__tests__/SavedPaymentTypes.spec.js @@ -0,0 +1,35 @@ +import SavedPaymentTypes from '../SavedPaymentTypes'; + +const add = jest.fn().mockName('esModuleObject.add'); + +const esModuleObject = jest + .fn() + .mockName('esModuleObject') + .mockReturnValue({ + add + }); + +const venia = { + esModuleObject +}; + +test('Should return correct shape', () => { + const savedPaymentTypes = new SavedPaymentTypes(venia); + + expect(savedPaymentTypes).toMatchSnapshot(); +}); + +test('Should add new import when add is called', () => { + const savedPaymentTypes = new SavedPaymentTypes(venia); + + const paymentCode = 'braintree'; + const importPath = 'path/to/the/component.js'; + savedPaymentTypes.add({ + paymentCode, + importPath + }); + + expect(add).toHaveBeenCalledWith( + `import ${paymentCode} from '${importPath}'` + ); +}); diff --git a/packages/venia-ui/lib/targets/__tests__/__snapshots__/SavedPaymentTypes.spec.js.snap b/packages/venia-ui/lib/targets/__tests__/__snapshots__/SavedPaymentTypes.spec.js.snap new file mode 100644 index 0000000000..374ab24583 --- /dev/null +++ b/packages/venia-ui/lib/targets/__tests__/__snapshots__/SavedPaymentTypes.spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should return correct shape 1`] = ` +SavedPaymentTypes { + "_methods": Object { + "add": [MockFunction esModuleObject.add], + }, +} +`; diff --git a/packages/venia-ui/lib/targets/__tests__/venia-ui-targets.spec.js b/packages/venia-ui/lib/targets/__tests__/venia-ui-targets.spec.js index 9937e5016c..790ee034e7 100644 --- a/packages/venia-ui/lib/targets/__tests__/venia-ui-targets.spec.js +++ b/packages/venia-ui/lib/targets/__tests__/venia-ui-targets.spec.js @@ -99,18 +99,18 @@ test('uses routes to inject client-routed pages', async () => { expect(built.bundle).toContain('DynamicCheckout'); }); -test('declares payments target', async () => { +test('declares checkoutPagePaymentTypes target', async () => { const bus = mockBuildBus({ context: __dirname, dependencies: [thisDep] }); bus.runPhase('declare'); - const { payments } = bus.getTargetsOf('@magento/venia-ui'); + const { checkoutPagePaymentTypes } = bus.getTargetsOf('@magento/venia-ui'); const interceptor = jest.fn(); // no implementation testing in declare phase - payments.tap('test', interceptor); - payments.call('woah'); + checkoutPagePaymentTypes.tap('test', interceptor); + checkoutPagePaymentTypes.call('woah'); expect(interceptor).toHaveBeenCalledWith('woah'); }); @@ -125,36 +125,21 @@ test('uses RichContentRenderers to default strategy Payment Method', async () => } ); - const payments = built.run(); - expect(payments).toHaveProperty('braintree'); + const checkoutPagePaymentTypes = built.run(); + expect(checkoutPagePaymentTypes).toHaveProperty('braintree'); }); -test('declares payments target', async () => { +test('declares savedPaymentTypes target', async () => { const bus = mockBuildBus({ context: __dirname, dependencies: [thisDep] }); bus.runPhase('declare'); - const { payments } = bus.getTargetsOf('@magento/venia-ui'); + const { savedPaymentTypes } = bus.getTargetsOf('@magento/venia-ui'); const interceptor = jest.fn(); // no implementation testing in declare phase - payments.tap('test', interceptor); - payments.call('woah'); + savedPaymentTypes.tap('test', interceptor); + savedPaymentTypes.call('woah'); expect(interceptor).toHaveBeenCalledWith('woah'); }); - -test('uses RichContentRenderers to default strategy Payment Method', async () => { - jest.setTimeout(WEBPACK_BUILD_TIMEOUT); - - const built = await buildModuleWith( - '../../components/CheckoutPage/PaymentInformation/paymentMethodCollection.js', - { - context: __dirname, - dependencies: ['@magento/peregrine', thisDep] - } - ); - - const payments = built.run(); - expect(payments).toHaveProperty('braintree'); -}); diff --git a/packages/venia-ui/lib/targets/venia-ui-declare.js b/packages/venia-ui/lib/targets/venia-ui-declare.js index 19eadf58aa..fb85ba8ed0 100644 --- a/packages/venia-ui/lib/targets/venia-ui-declare.js +++ b/packages/venia-ui/lib/targets/venia-ui-declare.js @@ -66,25 +66,48 @@ module.exports = targets => { routes: new targets.types.AsyncSeriesWaterfall(['routes']), /** - * Provides access to Venia's payment methods + * Provides access to Venia's checkout page payment methods * - * This target lets you add new payment to your storefronts. + * This target lets you add new checkout page payment to your storefronts. * * @member {tapable.SyncHook} * * @see [Intercept function signature]{@link paymentInterceptFunction} - * @see [PaymentMethodList]{@link #PaymentMethodList} - * @see [Payment definition object]{@link PaymentDefinition} + * @see [CheckoutPaymentTypes]{@link #CheckoutPaymentTypesDefinition} + * @see [CheckoutPayment definition object]{@link CheckoutPaymentDefinition} * * @example Add a payment - * targets.of('@magento/venia-ui').payments.tap( - * payments => payments.add({ + * targets.of('@magento/venia-ui').checkoutPagePaymentTypes.tap( + * checkoutPagePaymentTypes => checkoutPagePaymentTypes.add({ * paymentCode: 'braintree', * importPath: '@magento/braintree_payment' * }) * ); */ - payments: new targets.types.Sync(['payments']) + checkoutPagePaymentTypes: new targets.types.Sync([ + 'checkoutPagePaymentTypes' + ]), + + /** + * Provides access to Venia's saved payment methods + * + * This target lets you add new saved payment method to your storefronts. + * + * @member {tapable.SyncHook} + * + * @see [Intercept function signature]{@link savedPaymentInterceptFunction} + * @see [SavedPaymentTypes]{@link #SavedPaymentTypesDefinition} + * @see [SavedPayment definition object]{@link SavedPaymentDefinition} + * + * @example Add a payment + * targets.of('@magento/venia-ui').savedPaymentTypes.tap( + * savedPaymentTypes => savedPaymentTypes.add({ + * paymentCode: 'braintree', + * importPath: '@magento/braintree_payment' + * }) + * ); + */ + savedPaymentTypes: new targets.types.Sync(['savedPaymentTypes']) }); }; @@ -181,24 +204,24 @@ module.exports = targets => { * } */ -/** Type definition related to: payments */ +/** Type definition related to: checkoutPagePaymentTypes */ /** - * Intercept function signature for the `payments` target. + * Intercept function signature for the `checkoutPagePaymentTypes` target. * - * Interceptors of `payments` should call `.add` on the provided [payment list]{@link #PaymentMethodList}. + * Interceptors of `checkoutPagePaymentTypes` should call `.add` on the provided [payment list]{@link #CheckoutPaymentTypesDefinition}. * * @callback paymentInterceptFunction * - * @param {PaymentMethodList} renderers The list of payments registered + * @param {CheckoutPaymentTypesDefinition} renderers The list of payments registered * so far in the build. * */ /** - * A payment definition object that describes a payment in your storefront. + * A payment definition object that describes a checkout page payment in your storefront. * - * @typedef {Object} PaymentDefinition + * @typedef {Object} CheckoutPaymentDefinition * @property {string} paymentCode is use to map your payment * @property {string} importPath Resolvable path to the component the * Route component will render @@ -210,24 +233,24 @@ module.exports = targets => { * } */ -/** Type definition related to: payments */ +/** Type definition related to: savedPaymentTypes */ /** - * Intercept function signature for the `payments` target. + * Intercept function signature for the `savedPaymentTypes` target. * - * Interceptors of `payments` should call `.add` on the provided [payment list]{@link #PaymentMethodList}. + * Interceptors of `savedPaymentTypes` should call `.add` on the provided [payment list]{@link #SavedPaymentTypesDefinition}. * - * @callback paymentInterceptFunction + * @callback savedPaymentInterceptFunction * - * @param {PaymentMethodList} renderers The list of payments registered + * @param {SavedPaymentTypesDefinition} renderers The list of saved payments registered * so far in the build. * */ /** - * A payment definition object that describes a payment in your storefront. + * A payment definition object that describes a saved payment in your storefront. * - * @typedef {Object} PaymentDefinition + * @typedef {Object} SavedPaymentDefinition * @property {string} paymentCode is use to map your payment * @property {string} importPath Resolvable path to the component the * Route component will render @@ -235,6 +258,6 @@ module.exports = targets => { * @example A custom payment method * const myCustomPayment = { * paymentCode: 'cc', - * importPath: '@partner/module/path_to_your_component' + * importPath: '@partner/module/path_to_your_component' * } */ diff --git a/packages/venia-ui/lib/targets/venia-ui-intercept.js b/packages/venia-ui/lib/targets/venia-ui-intercept.js index 4d41478a1c..cf2ea3f59a 100644 --- a/packages/venia-ui/lib/targets/venia-ui-intercept.js +++ b/packages/venia-ui/lib/targets/venia-ui-intercept.js @@ -4,7 +4,8 @@ const { Targetables } = require('@magento/pwa-buildpack'); const RichContentRendererList = require('./RichContentRendererList'); const makeRoutesTarget = require('./makeRoutesTarget'); -const PaymentMethodList = require('./PaymentMethodList'); +const CheckoutPagePaymentsList = require('./CheckoutPagePaymentsList'); +const SavedPaymentTypes = require('./SavedPaymentTypes'); module.exports = veniaTargets => { const venia = Targetables.using(veniaTargets); @@ -27,10 +28,17 @@ module.exports = veniaTargets => { importPath: './plainHtmlRenderer' }); - const paymentMethodList = new PaymentMethodList(venia); - paymentMethodList.add({ + const checkoutPagePaymentsList = new CheckoutPagePaymentsList(venia); + checkoutPagePaymentsList.add({ paymentCode: 'braintree', importPath: '@magento/venia-ui/lib/components/CheckoutPage/PaymentInformation/creditCard' }); + + const savedPaymentTypes = new SavedPaymentTypes(venia); + savedPaymentTypes.add({ + paymentCode: 'braintree', + importPath: + '@magento/venia-ui/lib/components/SavedPaymentsPage/creditCard' + }); };