diff --git a/packages/extensions/venia-sample-payments-cashondelivery/intercept.js b/packages/extensions/venia-sample-payments-cashondelivery/intercept.js new file mode 100644 index 0000000000..1042660671 --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/intercept.js @@ -0,0 +1,41 @@ +module.exports = targets => { + const { specialFeatures } = targets.of('@magento/pwa-buildpack'); + specialFeatures.tap(flags => { + /** + * Wee need to activate esModules, cssModules and GQL Queries to allow build pack to load our extension + * {@link https://magento.github.io/pwa-studio/pwa-buildpack/reference/configure-webpack/#special-flags}. + */ + flags[targets.name] = { + esModules: true, + cssModules: true, + graphqlQueries: true + }; + }); + + const { + checkoutPagePaymentTypes, + editablePaymentTypes, + summaryPagePaymentTypes + } = targets.of('@magento/venia-ui'); + checkoutPagePaymentTypes.tap(payments => + payments.add({ + paymentCode: 'cashondelivery', + importPath: + '@magento/venia-sample-payments-cashondelivery/src/components/cashondelivery.js' + }) + ); + editablePaymentTypes.tap(editablePaymentTypes => { + editablePaymentTypes.add({ + paymentCode: 'cashondelivery', + importPath: + '@magento/venia-sample-payments-cashondelivery/src/components/editcod.js' + }); + }); + summaryPagePaymentTypes.tap(paymentSummaries => + paymentSummaries.add({ + paymentCode: 'cashondelivery', + importPath: + '@magento/venia-sample-payments-cashondelivery/src/components/summarycod.js' + }) + ); +}; diff --git a/packages/extensions/venia-sample-payments-cashondelivery/package.json b/packages/extensions/venia-sample-payments-cashondelivery/package.json new file mode 100644 index 0000000000..eb3b7f2d06 --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/package.json @@ -0,0 +1,27 @@ +{ + "name": "@magento/venia-sample-payments-cashondelivery", + "version": "0.0.1", + "publishConfig": { + "access": "public" + }, + "description": "Provides demo Cash On Delivery Payment Method for PWA Studio.", + "main": "./intercept.js", + "scripts": { + "clean": " " + }, + "repository": "github:magento/pwa-studio", + "license": "(OSL-3.0 OR AFL-3.0)", + "peerDependencies": { + "@magento/peregrine": "~14.4.1", + "@magento/pwa-buildpack": "~11.5.3", + "@magento/venia-ui": "~11.5.0", + "react": "~17.0.1", + "react-intl": "~5.20.0", + "react-router-dom": "~5.2.0" + }, + "pwa-studio": { + "targets": { + "intercept": "./intercept" + } + } +} diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/__snapshots__/cashondelivery.spec.js.snap b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/__snapshots__/cashondelivery.spec.js.snap new file mode 100644 index 0000000000..d2e81b2ea4 --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/__snapshots__/cashondelivery.spec.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should render with mailingAddress payable 1`] = ` +
+ +
+`; + +exports[`Should return correct shape 1`] = ` +
+ +
+`; diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/__snapshots__/editcod.spec.js.snap b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/__snapshots__/editcod.spec.js.snap new file mode 100644 index 0000000000..83573fbdbc --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/__snapshots__/editcod.spec.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+ +
+`; diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/__snapshots__/summarycod.spec.js.snap b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/__snapshots__/summarycod.spec.js.snap new file mode 100644 index 0000000000..26fcf254fd --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/__snapshots__/summarycod.spec.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+
+
+
+
+ + + +
+ + +
+
+ +
+ +
+
+`; diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/cashondelivery.spec.js b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/cashondelivery.spec.js new file mode 100644 index 0000000000..2ae1723831 --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/cashondelivery.spec.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { createTestInstance } from '@magento/peregrine'; +import { useCashondelivery } from '../../talons/useCashondelivery'; + +import CashOnDelivery from '../cashondelivery'; + +jest.mock('@magento/venia-ui/lib/classify'); +jest.mock('../../talons/useCashondelivery', () => { + return { + useCashondelivery: jest.fn().mockReturnValue({ + onBillingAddressChangedError: jest.fn(), + onBillingAddressChangedSuccess: jest.fn() + }) + }; +}); + +jest.mock( + '@magento/venia-ui/lib/components/CheckoutPage/BillingAddress', + () => props => +); + +const useCashondeliveryReturnValue = { + onBillingAddressChangedError: jest.fn(), + onBillingAddressChangedSuccess: jest.fn() +}; + +test('Should render CashOnDelivery component correctly', () => { + useCashondelivery.mockReturnValueOnce({ + ...useCashondeliveryReturnValue + }); + + const tree = createTestInstance(); + + expect(tree.toJSON()).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/editcod.spec.js b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/editcod.spec.js new file mode 100644 index 0000000000..baa11cd9da --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/editcod.spec.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { createTestInstance } from '@magento/peregrine'; + +import EditCod from '../editcod'; + +jest.mock('@magento/venia-ui/lib/classify'); +jest.mock('../cashondelivery', () => props => ); + +const mocks = { + onPaymentReady: jest.fn().mockName('onPaymentReady'), + onPaymentSuccess: jest.fn().mockName('onPaymentSuccess'), + onPaymentError: jest.fn().mockName('onPaymentError'), + resetShouldSubmit: jest.fn().mockName('resetShouldSubmit') +}; + +test('renders correctly', () => { + // Arrange. + const props = { + ...mocks, + shouldSubmit: false + }; + + // Act. + const tree = createTestInstance(); + + // Assert. + expect(tree.toJSON()).toMatchSnapshot(); +}); diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/summarycod.spec.js b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/summarycod.spec.js new file mode 100644 index 0000000000..8cdf105c3d --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/__tests__/summarycod.spec.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { createTestInstance } from '@magento/peregrine'; + +import SummaryCod from '../summarycod'; + +jest.mock('@magento/venia-ui/lib/classify'); +jest.mock('react-intl', () => ({ + FormattedMessage: props => ( +
+ ) +})); +jest.mock('@magento/venia-ui/lib/components/LinkButton', () => props => ( + +)); +jest.mock('@magento/venia-ui/lib/components/Icon', () => props => ( + +)); + +test('renders correctly', () => { + // Arrange. + const props = { + onEdit: jest.fn().mockName('onEdit') + }; + + // Act. + const tree = createTestInstance(); + + // Assert. + expect(tree.toJSON()).toMatchSnapshot(); +}); diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/cashondelivery.js b/packages/extensions/venia-sample-payments-cashondelivery/src/components/cashondelivery.js new file mode 100644 index 0000000000..9033ae2955 --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/cashondelivery.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { useStyle } from '@magento/venia-ui/lib/classify'; +import { shape, string, bool, func } from 'prop-types'; +import BillingAddress from '@magento/venia-ui/lib/components/CheckoutPage/BillingAddress'; +import { useCashondelivery } from '../talons/useCashondelivery'; +import defaultClasses from './cashondelivery.module.css'; + +/** + * The CashOnDelivery component renders all information to handle cashondelivery payment. + * + * @param {Boolean} props.shouldSubmit boolean value which represents if a payment nonce request has been submitted + * @param {Function} props.onPaymentSuccess callback to invoke when the a payment nonce has been generated + * @param {Function} props.onPaymentReady callback to invoke when the component is ready + * @param {Function} props.onPaymentError callback to invoke when component throws an error + * @param {Function} props.resetShouldSubmit callback to reset the shouldSubmit flag + */ +const CashOnDelivery = props => { + const classes = useStyle(defaultClasses, props.classes); + + const { + onBillingAddressChangedError, + onBillingAddressChangedSuccess + } = useCashondelivery(props); + + return ( + +
+ +
+ ); +}; + +CashOnDelivery.propTypes = { + classes: shape({ root: string }), + shouldSubmit: bool.isRequired, + onPaymentSuccess: func, + onPaymentReady: func, + onPaymentError: func, + resetShouldSubmit: func.isRequired +}; + +export default CashOnDelivery; diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/cashondelivery.module.css b/packages/extensions/venia-sample-payments-cashondelivery/src/components/cashondelivery.module.css new file mode 100644 index 0000000000..74b6bf3b50 --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/cashondelivery.module.css @@ -0,0 +1,3 @@ +.cod_root { + padding-top: 1.125rem; +} \ No newline at end of file diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/editcod.js b/packages/extensions/venia-sample-payments-cashondelivery/src/components/editcod.js new file mode 100644 index 0000000000..4c2a7ec7f7 --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/editcod.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { shape, string, bool, func } from 'prop-types'; + +import { useStyle } from '@magento/venia-ui/lib/classify'; + +import CashOnDelivery from './cashondelivery'; +import defaultClasses from './cashondelivery.module.css'; + +/** + * The edit view for the Cod payment method. + */ +const EditCod = props => { + const { + onPaymentReady, + onPaymentSuccess, + onPaymentError, + resetShouldSubmit, + shouldSubmit + } = props; + + const classes = useStyle(defaultClasses, props.classes); + + return ( +
+ +
+ ); +}; + +export default EditCod; + +EditCod.propTypes = { + classes: shape({ + root: string + }), + onPaymentReady: func.isRequired, + onPaymentSuccess: func.isRequired, + onPaymentError: func.isRequired, + resetShouldSubmit: func.isRequired, + shouldSubmit: bool +}; diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/summary.module.css b/packages/extensions/venia-sample-payments-cashondelivery/src/components/summary.module.css new file mode 100644 index 0000000000..179532469f --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/summary.module.css @@ -0,0 +1,42 @@ +.root { + display: grid; + gap: 1.125rem; + padding: 2rem; +} + +.heading_container { + display: grid; + grid-auto-flow: column; + grid-template-columns: 1fr; +} + +.heading { + font-weight: 600; +} + +.edit_button { + color: rgb(var(--venia-brand-color-1-700)); + padding: 1rem; + margin: -1rem; +} + +.edit_icon { + stroke: rgb(var(--venia-brand-color-1-700)); +} + +.edit_text { +} + +.cod_details_container { + display: grid; + gap: 0.5rem; +} + +.payment_type { +} + +@media screen(-lg) { + .edit_text { + display: none; + } +} diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/components/summarycod.js b/packages/extensions/venia-sample-payments-cashondelivery/src/components/summarycod.js new file mode 100644 index 0000000000..346514048d --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/components/summarycod.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { func, shape, string } from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Edit2 as EditIcon } from 'react-feather'; +import { useStyle } from '@magento/venia-ui/lib/classify'; +import Icon from '@magento/venia-ui/lib/components/Icon'; +import LinkButton from '@magento/venia-ui/lib/components/LinkButton'; + +import defaultClasses from './summary.module.css'; + +/** + * The SummaryCod component of the Cash On Delivery payment method extension. + */ +const SummaryCod = props => { + const { onEdit } = props; + + const classes = useStyle(defaultClasses, props.classes); + + return ( +
+
+
+ +
+ + + + + + +
+
+ + + +
+
+ ); +}; + +export default SummaryCod; + +SummaryCod.propTypes = { + classes: shape({ + root: string, + cod_details_container: string, + edit_button: string, + edit_icon: string, + edit_text: string, + heading_container: string, + heading: string, + payment_type: string + }), + onEdit: func +}; diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/talons/cashondelivery.gql.js b/packages/extensions/venia-sample-payments-cashondelivery/src/talons/cashondelivery.gql.js new file mode 100644 index 0000000000..fe2cff7490 --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/talons/cashondelivery.gql.js @@ -0,0 +1,21 @@ +import { gql } from '@apollo/client'; + +export const SET_COD_PAYMENT_METHOD_ON_CART = gql` + mutation setPaymentMethodOnCart($cartId: String!) { + setPaymentMethodOnCart( + input: { cart_id: $cartId, payment_method: { code: "cashondelivery" } } + ) { + cart { + id + selected_payment_method { + code + title + } + } + } + } +`; + +export default { + setCodPaymentMethodOnCartMutation: SET_COD_PAYMENT_METHOD_ON_CART +}; diff --git a/packages/extensions/venia-sample-payments-cashondelivery/src/talons/useCashondelivery.js b/packages/extensions/venia-sample-payments-cashondelivery/src/talons/useCashondelivery.js new file mode 100644 index 0000000000..623c9bb16a --- /dev/null +++ b/packages/extensions/venia-sample-payments-cashondelivery/src/talons/useCashondelivery.js @@ -0,0 +1,82 @@ +import { useCallback, useEffect } from 'react'; +import { useQuery, useMutation } from '@apollo/client'; +import mergeOperations from '@magento/peregrine/lib/util/shallowMerge'; +import { useCartContext } from '@magento/peregrine/lib/context/cart'; + +import DEFAULT_OPERATIONS from './cashondelivery.gql'; + +/** + * Talon to handle cod payment. + * + * @param {Boolean} props.shouldSubmit boolean value which represents if a payment nonce request has been submitted + * @param {Function} props.onPaymentSuccess callback to invoke when the a payment nonce has been generated + * @param {Function} props.onPaymentError callback to invoke when component throws an error + * @param {Function} props.resetShouldSubmit callback to reset the shouldSubmit flag + * @param {DocumentNode} props.operations.setCodPaymentMethodOnCartMutation mutation to set cashondelivery as payment + * + * @returns { + * onBillingAddressChangedError: Function, + * onBillingAddressChangedSuccess: Function + * } + */ +export const useCashondelivery = props => { + const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations); + + const { + setCodPaymentMethodOnCartMutation + } = operations; + + const [{ cartId }] = useCartContext(); + + const { resetShouldSubmit, onPaymentSuccess, onPaymentError } = props; + + const [ + updatePaymentMethod, + { + error: paymentMethodMutationError, + called: paymentMethodMutationCalled, + loading: paymentMethodMutationLoading + } + ] = useMutation(setCodPaymentMethodOnCartMutation); + + /** + * This function will be called if cant not set address. + */ + const onBillingAddressChangedError = useCallback(() => { + resetShouldSubmit(); + }, [resetShouldSubmit]); + + /** + * This function will be called if address was successfully set. + */ + const onBillingAddressChangedSuccess = useCallback(() => { + updatePaymentMethod({ + variables: { cartId } + }); + }, [updatePaymentMethod, cartId]); + + useEffect(() => { + const paymentMethodMutationCompleted = + paymentMethodMutationCalled && !paymentMethodMutationLoading; + + if (paymentMethodMutationCompleted && !paymentMethodMutationError) { + onPaymentSuccess(); + } + + if (paymentMethodMutationCompleted && paymentMethodMutationError) { + onPaymentError(); + } + }, [ + paymentMethodMutationError, + paymentMethodMutationLoading, + paymentMethodMutationCalled, + onPaymentSuccess, + onPaymentError, + resetShouldSubmit + ]); + + return { + onBillingAddressChangedError, + onBillingAddressChangedSuccess + }; +};