UNPKG

@shopgate/engage

Version:
197 lines (188 loc) • 6.3 kB
import React, { useContext, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { connect, useStore } from 'react-redux'; import PropTypes from 'prop-types'; import { css } from 'glamor'; import { themeConfig } from '@shopgate/pwa-common/helpers/config'; import { getPaymentMethods } from "../selectors/payment"; import { getCheckoutOrder } from "../selectors/order"; import CheckoutContext from "../providers/CheckoutProvider.context"; import { updateCheckoutOrder } from "../actions/updateCheckoutOrder"; import { fetchCheckoutOrder } from "../actions/fetchCheckoutOrder"; import { i18n } from "../../core/helpers/i18n"; import Context from "./context"; import paypal from "./paypal"; import stripe from "./stripe"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const AVAILABLE_PAYMENT_METHOD = [paypal, stripe]; /** * Maps state to props * @param {Object} state State * @returns {Object} */ const mapStateToProps = state => ({ order: getCheckoutOrder(state), paymentMethods: getPaymentMethods(state) }); /** * Dispatch * @param {Object} dispatch Dispatch * @returns {Object} */ const mapDispatchToProps = dispatch => ({ updateOrder: order => dispatch(updateCheckoutOrder(order)), fetchOrder: () => dispatch(fetchCheckoutOrder()) }); const { variables } = themeConfig; const styles = { headline: css({ fontSize: '1.25rem', fontWeight: 'normal', margin: `0 0 ${variables.gap.small}px 0`, marginLeft: 16, marginRight: 8, color: 'var(--color-text-high-emphasis)', textTransform: 'none' }).toString(), section: css({ marginBottom: 0, marginTop: 4 }).toString(), buttons: css({ marginLeft: 16, marginRight: 16, marginBottom: 16, display: 'flex', flexDirection: 'row' }).toString() }; /** * PaymentMethodProvider * @param {Object} props Props * @returns {JSX} */ const PaymentMethodProvider = ({ order, paymentMethods, fetchOrder, updateOrder }) => { const [activePaymentMeta, setActivePaymentMeta] = useState(null); const { setPaymentHandler, setPaymentData, needsPayment, setButtonLocked, setLocked } = useContext(CheckoutContext); const paymentMethodRef = useRef(); // Set active payment method to the orders first transaction. const paymentMethodCode = useMemo(() => { const transaction = order.paymentTransactions?.[0]; if (!transaction) return null; return transaction.paymentMethod.code; }, [order]); const paymentData = useMemo(() => paymentMethods?.find(p => p.code === paymentMethodCode), [paymentMethodCode, paymentMethods]); const paymentImpl = useMemo(() => AVAILABLE_PAYMENT_METHOD.find(method => method.code === paymentData?.paymentProvider?.code), [paymentData]); // Global transaction handler // Currently simply redirect fulfill request to the active payment method. useEffect(() => { setPaymentHandler({ getSupportsRedirect: paymentImpl?.getSupportsRedirect || (() => true), getCustomPayButton: () => paymentImpl?.payButton, fulfillTransaction: async ({ paymentTransactions }) => { const resolved = await paymentMethodRef.current.fulfillTransaction({ paymentTransactions }); return resolved; } }); }, [paymentImpl, setButtonLocked, setPaymentHandler]); // Map configured payment methods const availablePaymentMethods = useMemo(() => paymentMethods.map(method => ({ ...AVAILABLE_PAYMENT_METHOD.find(m => m.code === method.paymentProvider.code), internalCode: method.code, settings: method.settings })), [paymentMethods]); // Change payment method. const handleChangePayment = useCallback(async (code, meta = null) => { if (paymentMethodCode === code) { setActivePaymentMeta(meta); setPaymentData({ meta }); return; } setLocked(true); await updateOrder({ paymentTransactions: [{ paymentMethod: { code } }] }); await fetchOrder(); setActivePaymentMeta(meta); setPaymentData({ meta }); setLocked(false); }, [setPaymentData, paymentMethodCode, setLocked, updateOrder, fetchOrder]); // API for the underlying payment methods. const paymentMethodApi = useMemo(() => ({ registerPaymentMethod: api => { setButtonLocked(false); paymentMethodRef.current = api; } }), [setButtonLocked]); const store = useStore(); // Ignore for ROPIS. if (!needsPayment) { return null; } // Render the respective payment method provider. const { provider: Provider, content: Content } = paymentImpl || {}; return /*#__PURE__*/_jsx(Context.Provider, { value: paymentMethodApi, children: /*#__PURE__*/_jsxs("div", { className: styles.section, children: [/*#__PURE__*/_jsx("h3", { className: styles.headline, children: i18n.text('checkout.payment.title') }), /*#__PURE__*/_jsx("div", { className: styles.buttons, children: availablePaymentMethods.map(method => /*#__PURE__*/_jsx(method.button, { settings: method.settings, onChange: meta => handleChangePayment(method.internalCode, meta), active: method.internalCode === paymentMethodCode, activePaymentMeta: activePaymentMeta }, method.internalCode)) }), paymentImpl ? /*#__PURE__*/_jsx(Provider /** * 2025-01-10: Not 100% sure why a context is being passed here. It seems to work * without it, but since the payment component implementation has a high complexity, * but isn't really used right now in production shops, i kept it for now. * To enable compatibility with react-redux > 7, the "store" prop was added so that * Redux connected child components can still access the store. * Should be revisited when the "native checkout" gets relevance. */, { context: Context, store: store, data: paymentData, activePaymentMeta: activePaymentMeta, children: /*#__PURE__*/_jsx(Content, {}) }) : null] }) }); }; PaymentMethodProvider.defaultProps = { order: null, paymentMethods: null }; export default connect(mapStateToProps, mapDispatchToProps)(PaymentMethodProvider);