diff --git a/renderers.js b/renderers.js index 28266ac..1aafb59 100644 --- a/renderers.js +++ b/renderers.js @@ -1,8 +1,11 @@ +import { applePayType } from './src/utility/config'; import DefaultRenderer from './src/components/DefaultRenderer'; +import ApplePayRenderer from './src/components/ApplePayRenderer'; import CreditCardComponentsRenderer from './src/components/CreditCardComponentsRenderer'; export default { - mollie_methods_applepay: DefaultRenderer, + mollie_methods_applepay: + applePayType === 'direct' ? ApplePayRenderer : DefaultRenderer, mollie_methods_bancontact: DefaultRenderer, mollie_methods_banktransfer: DefaultRenderer, mollie_methods_belfius: DefaultRenderer, diff --git a/src/api/applePayValidationRequest/applePayValidationRequest.js b/src/api/applePayValidationRequest/applePayValidationRequest.js new file mode 100644 index 0000000..95ee974 --- /dev/null +++ b/src/api/applePayValidationRequest/applePayValidationRequest.js @@ -0,0 +1,18 @@ +import sendRequest from '../../../../../api/sendRequest'; +import modifier from './modifier'; +import { MOLLIE_APPLE_PAY_VALIDATION } from './mutation'; + +export default async function applePayValidationRequest( + appDispatch, + validationURL +) { + return modifier( + await sendRequest(appDispatch, { + query: MOLLIE_APPLE_PAY_VALIDATION, + variables: { + domain: window.location.hostname, + validationURL, + }, + }) + ); +} diff --git a/src/api/applePayValidationRequest/index.js b/src/api/applePayValidationRequest/index.js new file mode 100644 index 0000000..7d41a15 --- /dev/null +++ b/src/api/applePayValidationRequest/index.js @@ -0,0 +1,3 @@ +import applePayValidationRequest from './applePayValidationRequest'; + +export default applePayValidationRequest; diff --git a/src/api/applePayValidationRequest/modifier.js b/src/api/applePayValidationRequest/modifier.js new file mode 100644 index 0000000..a04eb02 --- /dev/null +++ b/src/api/applePayValidationRequest/modifier.js @@ -0,0 +1,5 @@ +import { get as _get } from 'lodash-es'; + +export default function applePayValidationRequestModifier(response) { + return _get(response, 'data.mollieApplePayValidation.response', {}); +} diff --git a/src/api/applePayValidationRequest/mutation.js b/src/api/applePayValidationRequest/mutation.js new file mode 100644 index 0000000..eecfadd --- /dev/null +++ b/src/api/applePayValidationRequest/mutation.js @@ -0,0 +1,13 @@ +export const MOLLIE_APPLE_PAY_VALIDATION = ` + mutation mollieApplePayValidation( + $domain: String! + $validationURL: String! + ) { + mollieApplePayValidation( + domain: $domain + validationUrl: $validationURL + ) { + response + } + } +`; diff --git a/src/api/setPaymentMethodOnCartRequest/mutation.js b/src/api/setPaymentMethodOnCartRequest/mutation.js index df1d984..788668e 100644 --- a/src/api/setPaymentMethodOnCartRequest/mutation.js +++ b/src/api/setPaymentMethodOnCartRequest/mutation.js @@ -10,6 +10,7 @@ export const SET_PAYMENT_METHOD_ON_CART = ` $paymentCode: String! $issuer: String, $cardToken: String + $applePayToken: String ) { setPaymentMethodOnCart( input: { @@ -17,7 +18,8 @@ export const SET_PAYMENT_METHOD_ON_CART = ` payment_method: { code: $paymentCode mollie_selected_issuer: $issuer, - mollie_card_token: $cardToken + mollie_card_token: $cardToken, + mollie_applepay_payment_token: $applePayToken } } ) { diff --git a/src/components/ApplePayRenderer.jsx b/src/components/ApplePayRenderer.jsx new file mode 100644 index 0000000..2241aa2 --- /dev/null +++ b/src/components/ApplePayRenderer.jsx @@ -0,0 +1,76 @@ +import React, { useEffect, useCallback } from 'react'; +import { shape, func } from 'prop-types'; +import { paymentMethodShape } from '../../../../utils/payment'; +import RadioInput from '../../../../components/common/Form/RadioInput'; +import useCheckoutFormContext from '../../../../hook/useCheckoutFormContext'; +import useApplePayToPlaceOrder from '../hooks/useApplePayToPlaceOrder'; +import usePaymentMethodFormContext from '../../../../components/paymentMethod/hooks/usePaymentMethodFormContext'; + +function ApplePayRenderer({ method, selected, actions }) { + const isSelected = method.code === selected.code; + + const { registerPaymentAction } = useCheckoutFormContext(); + const { applePayPlaceOrder } = useApplePayToPlaceOrder(); + const { submitHandler } = usePaymentMethodFormContext(); + const submitHandlerCallback = useCallback(submitHandler, [submitHandler]); + + useEffect(() => { + if (!isSelected) { + return; + } + + registerPaymentAction(method.code, applePayPlaceOrder); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSelected, method.code]); + + useEffect(() => { + if (!isSelected) { + return; + } + + // Mark this method as selected + submitHandlerCallback(method.code); + }, [method.code, isSelected]); // eslint-disable-line react-hooks/exhaustive-deps + + try { + if (!window.ApplePaySession || !window.ApplePaySession.canMakePayments()) { + return null; + } + } catch (error) { + return null; + } + + if (!isSelected) { + return ( + + ); + } + + return ( +
+
+ +
+
+ ); +} + +ApplePayRenderer.propTypes = { + method: paymentMethodShape.isRequired, + selected: paymentMethodShape.isRequired, + actions: shape({ change: func }).isRequired, +}; + +export default ApplePayRenderer; diff --git a/src/hooks/useApplePayToPlaceOrder.js b/src/hooks/useApplePayToPlaceOrder.js new file mode 100644 index 0000000..3b1f979 --- /dev/null +++ b/src/hooks/useApplePayToPlaceOrder.js @@ -0,0 +1,76 @@ +import { useCallback, useContext } from 'react'; +import { get as _get } from 'lodash-es'; +import CartContext from '../../../../context/Cart/CartContext'; +import useMollieAppContext from './useMollieAppContext'; +import applePayValidationRequest from '../api/applePayValidationRequest'; +import useMolliePlaceOrder from './useMolliePlaceOrder'; +import { currencyCode, defaultCountry, storeName } from '../utility/config'; + +export default function useApplePayToPlaceOrder() { + const { appDispatch } = useMollieAppContext(); + const [cartData] = useContext(CartContext); + const cart = _get(cartData, 'cart'); + const prices = _get(cart, 'prices') || {}; + const { placeOrder } = useMolliePlaceOrder({ + methodCode: 'mollie_methods_applepay', + }); + + return { + applePayPlaceOrder: useCallback(() => { + let session; + + const request = { + countryCode: defaultCountry, + currencyCode, + + supportedNetworks: ['amex', 'maestro', 'masterCard', 'visa', 'vPay'], + merchantCapabilities: ['supports3DS'], + total: { + // TODO: Make this configurable + label: storeName, + amount: prices.grandTotalAmount, + }, + }; + + if (!session) { + // eslint-disable-next-line no-undef + session = new ApplePaySession(3, request); + } + + session.onpaymentmethodselected = () => { + session.completePaymentMethodSelection( + { + label: 'Total', + type: 'final', + amount: prices.grandTotalAmount, + }, + [] + ); + }; + + session.onvalidatemerchant = async (event) => { + const response = await applePayValidationRequest( + appDispatch, + event.validationURL + ); + + session.completeMerchantValidation(JSON.parse(response)); + }; + + session.onpaymentauthorized = (event) => { + try { + placeOrder({ applePayToken: JSON.stringify(event.payment.token) }); + } catch { + // eslint-disable-next-line no-undef + session.completePayment(ApplePaySession.STATUS_ERROR); + } + }; + + session.oncancel = () => { + session = null; + }; + + session.begin(); + }, [prices.grandTotalAmount, appDispatch, placeOrder]), + }; +} diff --git a/src/hooks/useMolliePlaceOrder.js b/src/hooks/useMolliePlaceOrder.js index bded4fa..5d7d86e 100644 --- a/src/hooks/useMolliePlaceOrder.js +++ b/src/hooks/useMolliePlaceOrder.js @@ -6,6 +6,8 @@ import useMollieCartContext from './useMollieCartContext'; import placeOrderRequest from '../api/placeOrderRequest'; import restoreCartRequest from '../api/restoreCartRequest'; import useMollieComponentsApi from './useMollieComponentsApi'; +import LocalStorage from '../../../../utils/localStorage'; +import { config } from '../../../../config'; export default function useMolliePlaceOrder({ methodCode, selectedIssuer }) { const { cartId, setCartInfo } = useMollieCartContext(); @@ -14,7 +16,7 @@ export default function useMolliePlaceOrder({ methodCode, selectedIssuer }) { useMollieComponentsApi(methodCode, setMollie); const placeOrder = useCallback( - async ({ token = null }) => { + async ({ token = null, applePayToken = null }) => { try { setPageLoader(true); await setPaymentMethodOnCartRequest(appDispatch, { @@ -22,6 +24,7 @@ export default function useMolliePlaceOrder({ methodCode, selectedIssuer }) { paymentCode: methodCode, issuer: selectedIssuer, cardToken: token, + applePayToken, }); } catch (error) { setPageLoader(false); @@ -36,6 +39,15 @@ export default function useMolliePlaceOrder({ methodCode, selectedIssuer }) { const { mollie_redirect_url: mollieRedirectUrl } = await placeOrderRequest(appDispatch); + if ( + !mollieRedirectUrl && + (methodCode === 'mollie_methods_applepay' || + methodCode === 'mollie_methods_creditcard') + ) { + LocalStorage.clearCheckoutStorage(); + window.location.replace(config.successPageRedirectUrl); + } + if (!mollieRedirectUrl) { throw Error('No redirect url found'); } diff --git a/src/utility/config.js b/src/utility/config.js index 1287e6a..2433581 100644 --- a/src/utility/config.js +++ b/src/utility/config.js @@ -1,9 +1,23 @@ import RootElement from '../../../../utils/rootElement'; +import env from '../../../../utils/env'; +import { config } from '../../../../config'; const paymentConfig = RootElement.getPaymentConfig(); const mollieConfig = paymentConfig.mollie || {}; const useComponents = mollieConfig?.creditcard?.use_components; +const applePayType = mollieConfig?.applepay?.integration_type; +const storeName = mollieConfig?.store?.name || 'Your Store Name'; + +const { code: rootCurrencyCode } = RootElement.getCurrency(); +const envCurrencyCode = env.currencyCode; + +const currencyCode = envCurrencyCode || rootCurrencyCode; + +const defaultCountry = + env.defaultCountry || + RootElement.getDefaultCountryId() || + config.defaultCountry; const { profile_id, // eslint-disable-line camelcase @@ -11,4 +25,13 @@ const { testmode, } = mollieConfig; -export { profile_id as profileId, useComponents, locale, testmode }; // eslint-disable-line camelcase +export { + profile_id as profileId, // eslint-disable-line camelcase + useComponents, + locale, + testmode, + applePayType, + currencyCode, + defaultCountry, + storeName, +};