Created
December 9, 2021 12:48
-
-
Save ArjenMiedema/95b08f5a86c959a34fc9a8a524bdb98a to your computer and use it in GitHub Desktop.
Mollie GraphQL implementation for Hyva Checkout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as actionStates from '../actions/actionStates'; | |
import {countriesGqlToFormik} from "../../utils/countriesGqlToFormik"; | |
export const appReducer = (state, action) => { | |
switch (action.type) { | |
(...) | |
case 'CREATE_MOLLIE_SUCCESS': | |
window.location.href = action?.payload?.createMollieTransaction?.checkout_url; | |
return true; | |
(...) | |
default: | |
return state; | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { config } from "../../config"; | |
import * as actionStates from "../actions/actionStates"; | |
export const cartReducer = (state, action) => { | |
if (action.data?.id) { | |
/** | |
* Always update the cart ID so that: | |
* 1: we always have the most recent ID | |
* 2: the customer cart id is replaced with the Masked Cart Token Id | |
*/ | |
config.cartId = action.data.id; | |
} | |
let cartData = {}; | |
let customerData = {}; | |
if (action.data) { | |
const { | |
data: { cart, customerCart, customer }, | |
} = action; | |
if (cart) { | |
cartData = { ...cart }; | |
} | |
if (customerCart) { | |
cartData = { ...customerCart }; | |
} | |
if (customer) { | |
customerData = { ...customer }; | |
} | |
} | |
switch (action.type) { | |
(...) | |
case "PLACE_ORDER_SUCCESS": | |
return { | |
...state, | |
errors: false, | |
cart: { | |
loaded: true, | |
...cartData, | |
shipping_addresses: action.payload.setShippingMethodsOnCart.cart.shipping_addresses, | |
selected_payment_method: action.payload.setPaymentMethodOnCart.cart.selected_payment_method | |
}, | |
agreements: { | |
...state.agreements, | |
}, | |
mollieToken: | |
action.payload.placeOrder.order.mollie_payment_token, | |
orderId: action.payload.placeOrder.order.order_number, | |
}; | |
(...) | |
default: | |
return state; | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const createMollieQuery = (paymentToken, issuer) => `mutation { | |
createMollieTransaction(input: { | |
payment_token: "${paymentToken}" | |
issuer: "${issuer}" | |
}) { | |
checkout_url | |
} | |
}`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {useState, useEffect} from "react"; | |
import {useCartContext} from "../../context/Cart"; | |
import {Form, FormikProvider, Field} from "formik"; | |
import {useFormikContext} from "../../context/Formik"; | |
import {RadioButton} from "./FormUI/RadioButton"; | |
import {motion, AnimateSharedLayout} from "framer-motion"; | |
// This replaces the "normal" PaymentMethods component | |
export const MolliePaymentMethods = () => { | |
const [{PaymentMethodForm}] = useFormikContext(); | |
const [{cart}, {setPaymentMethod}] = useCartContext(); | |
const [bankOptions, setBankOptions] = useState(false); | |
useEffect(() => { | |
setBankOptions(false); | |
}, [PaymentMethodForm.values.issuer]); | |
useEffect(() => { | |
if (PaymentMethodForm.values.code.length > 0) { | |
setPaymentMethod(PaymentMethodForm.values.code); | |
} | |
}, [PaymentMethodForm.values.code]); | |
useEffect(async () => { | |
const defaultPaymentMethod = cart.available_payment_methods[0]; | |
if (defaultPaymentMethod) { | |
await PaymentMethodForm.setFieldValue( | |
"code", | |
defaultPaymentMethod.code | |
); | |
} | |
}, [cart?.available_payment_methods]) | |
if ( | |
cart?.shipping_addresses && | |
cart?.shipping_addresses[0]?.available_shipping_methods | |
) { | |
return ( | |
<div className="mb-4"> | |
<header className={"text-xl font-bold mb-2"}> | |
Payment Methods | |
</header> | |
<ul> | |
<AnimateSharedLayout> | |
<FormikProvider value={PaymentMethodForm}> | |
<Form> | |
{cart?.available_payment_methods && | |
cart?.available_payment_methods.map( | |
(paymentOption) => { | |
return ( | |
<motion.li | |
layout | |
className={ | |
"mb-4 border border-solid border-gray-dark" | |
} | |
> | |
<label | |
className={ | |
"flex items-center p-4 space-x-2 font-bold cursor-pointer hover:border-gray" | |
} | |
htmlFor={ | |
paymentOption.code | |
} | |
> | |
<RadioButton | |
type={"radio"} | |
className={ | |
"form-radio" | |
} | |
name="code" | |
id={ | |
paymentOption.code | |
} | |
required={true} | |
value={ | |
paymentOption.code | |
} | |
/> | |
<span | |
className="flex items-center"> | |
{ | |
paymentOption?.logo && | |
<img | |
src={paymentOption.logo} | |
className="mr-2 h-5 w-auto" | |
/> | |
} | |
{paymentOption.title} | |
</span> | |
</label> | |
{/* If the payment method ideal is available and ideal is been selected */} | |
{paymentOption?.mollie_available_issuers && | |
paymentOption.code === | |
PaymentMethodForm | |
.values | |
.code && ( | |
<motion.div | |
type="crossfade" | |
initial={{ | |
height: "0", | |
}} | |
animate={{ | |
height: | |
"auto", | |
}} | |
exit={{ | |
height: "0", | |
}} | |
className={ | |
"bg-gray-light p-4" | |
} | |
> | |
<div | |
className={ | |
"relative" | |
} | |
> | |
<button | |
type="button" | |
onClick={() => | |
setBankOptions( | |
!bankOptions | |
) | |
} | |
aria-haspopup="listbox" | |
aria-expanded="true" | |
aria-labelledby="listbox-label" | |
className={ | |
"relative w-full bg-white border border-gray shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-green focus:border-green text-base" | |
} | |
> | |
{!PaymentMethodForm | |
.values | |
.issuer && ( | |
<div> | |
<div | |
className={ | |
"whitespace-no-wrap flex space-x-1 cursor-pointer" | |
} | |
> | |
<div | |
className={ | |
"bg-gray-100 w-6 h-6" | |
} | |
></div> | |
<span> | |
Select a Bank | |
</span> | |
</div> | |
</div> | |
)} | |
{PaymentMethodForm | |
.values | |
.issuer && | |
paymentOption.mollie_available_issuers | |
.filter( | |
( | |
b | |
) => | |
b.code === | |
PaymentMethodForm | |
.values | |
.issuer | |
) | |
.map( | |
( | |
bank | |
) => { | |
return ( | |
<div | |
className={ | |
"whitespace-no-wrap flex space-x-1 cursor-pointer" | |
} | |
> | |
<img | |
src={ | |
bank.image | |
} | |
alt={ | |
bank.name | |
} | |
className={ | |
"w-auto h-6" | |
} | |
/> | |
<span> | |
{ | |
bank.name | |
} | |
</span> | |
</div> | |
); | |
} | |
)} | |
<span | |
className={ | |
"ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none" | |
} | |
> | |
<svg | |
className="h-5 w-5 text-gray-dark" | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 20 20" | |
fill="currentColor" | |
aria-hidden="true" | |
> | |
<path | |
fill-rule="evenodd" | |
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" | |
clip-rule="evenodd" | |
/> | |
</svg> | |
</span> | |
</button> | |
{bankOptions && ( | |
<div | |
className="absolute mt-1 w-full bg-white shadow-lg"> | |
<ul | |
tabindex="-1" | |
role="listbox" | |
aria-labelledby="listbox-label" | |
aria-activedescendant="listbox-item-3" | |
className="max-h-56 py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-base" | |
> | |
{paymentOption.mollie_available_issuers | |
.filter( | |
( | |
b | |
) => | |
b.code | |
) | |
.map( | |
( | |
bank | |
) => { | |
return ( | |
<li | |
key={ | |
bank.code | |
} | |
role="option" | |
className={ | |
"text-gray-darkest cursor-default select-none relative hover:bg-gray-100 cursor-pointer" | |
} | |
> | |
<label | |
className={ | |
"py-2 pl-3 pr-9 whitespace-no-wrap flex space-x-1 cursor-pointer" | |
} | |
htmlFor={ | |
bank.code | |
} | |
> | |
<img | |
src={ | |
bank.image | |
} | |
alt={ | |
bank.name | |
} | |
className={ | |
"w-auto h-6" | |
} | |
/> | |
<Field | |
type={ | |
"radio" | |
} | |
className={ | |
"hidden" | |
} | |
name="issuer" | |
id={ | |
bank.code | |
} | |
value={ | |
bank.code | |
} | |
/> | |
<span> | |
{ | |
bank.name | |
} | |
</span> | |
</label> | |
{bank.code === | |
PaymentMethodForm | |
.values | |
.issuer && ( | |
<span | |
className="absolute inset-y-0 right-0 flex items-center pr-4"> | |
<svg | |
className="h-5 w-5" | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 20 20" | |
fill="currentColor" | |
aria-hidden="true" | |
> | |
<path | |
fill-rule="evenodd" | |
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" | |
clip-rule="evenodd" | |
/> | |
</svg> | |
</span> | |
)} | |
</li> | |
); | |
} | |
)} | |
</ul> | |
</div> | |
)} | |
</div> | |
</motion.div> | |
)} | |
</motion.li> | |
); | |
} | |
)} | |
</Form> | |
</FormikProvider> | |
</AnimateSharedLayout> | |
</ul> | |
{!PaymentMethodForm.isValid && ( | |
<div | |
id={`payment-methods-feedback`} | |
aria-live="polite" | |
className={"feedback text-sm text-left mt-3 text-red"} | |
> | |
No payment method selected or not all information is filled in. | |
</div> | |
)} | |
</div> | |
); | |
} | |
return ( | |
<div className="mb-4"> | |
<header className={"text-xl font-bold mb-2"}> | |
Payment Methods | |
</header> | |
<div className={""}> | |
<p>Enter your Personal Information to see the payment methods</p> | |
</div> | |
</div> | |
); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {createMollieQuery} from '../../graphql/app/createMollieQuery'; | |
import {graphqlRequest} from '../graphqlRequest'; | |
export const mollieRedirectAction = async (dispatch, token, issuer) => { | |
const query = createMollieQuery(token, issuer); | |
const type = 'CREATE_MOLLIE'; | |
const returnData = data => data.createMollieTransaction; | |
return await graphqlRequest(dispatch, query, type, returnData); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect } from "react"; | |
import { useCartContext } from "../../context/Cart"; | |
import { useFormikContext } from "../../context/Formik"; | |
import { useAppContext } from "../../context/App"; | |
import { config } from "../../config"; | |
import ArrowDownIcon from "../Icons/ArrowDown"; | |
import { useRef } from "react"; | |
import Loader from "./Loader"; | |
import LocalStorage from "../../utils/localStorage"; | |
export const PlaceOrder = () => { | |
const [ | |
{ cart, orderId, mollieToken }, | |
{ | |
placeOrder, | |
subscribeToNewsletter, | |
createAccount, | |
mergeCarts, | |
createEmptyCart, | |
setShippingAddress, | |
setShippingMethod, | |
setEmailOnGuestCart, | |
}, | |
] = useCartContext(); | |
const [ | |
{ | |
BillingAddressForm, | |
EmailForm, | |
NewsLetterForm, | |
PaymentMethodForm, | |
ShippingAddressForm, | |
ShippingMethodForm, | |
DeliveryInfoForm, | |
CheckoutAgreementsForm, | |
}, | |
{ touchAndValidateForm }, | |
] = useFormikContext(); | |
const [showLoader, setShowLoader] = useState(false); | |
const [ | |
{}, | |
{ mollieRedirect, fetchCustomerToken, doLoginAction }, | |
] = useAppContext(); | |
useEffect(() => { | |
if (orderId) { | |
if (mollieToken) { | |
mollieRedirect(mollieToken, PaymentMethodForm.values.issuer) | |
.then((data) => { | |
if (!data) { | |
setShowLoader(false); | |
} | |
}) | |
.catch((data) => { | |
setShowLoader(false); | |
}); | |
} else { | |
window.location.href = | |
config.baseUrl + "checkout/onepage/success"; | |
} | |
} | |
}, [orderId, mollieToken]); | |
const handleLogin = () => { | |
const formKey = document.querySelector('input[name="form_key"]').value; | |
return doLoginAction(formKey, EmailForm.values, false); | |
}; | |
const { loaded, items } = cart; | |
let btnRef = useRef(); | |
const canPlaceOrder = () => { | |
return loaded && items?.length; | |
}; | |
const submitHandler = async () => { | |
setShowLoader(true); | |
const EmailFormIsValid = await touchAndValidateForm(EmailForm) | |
.then(() => { | |
if (cart.email !== null) return true; | |
// in case you fill out everything except the email. | |
// and then press the order button. We dont have time to set the guest email. | |
// therefore we do it here. | |
return setEmailOnGuestCart(EmailForm.values.email).then(() => { | |
return true; | |
}); | |
}) | |
.catch(() => { | |
window.dispatchMessages && | |
window.dispatchMessages( | |
[ | |
{ | |
type: "warning", | |
text: "Enter your email address", | |
}, | |
], | |
5000 | |
); | |
return false; | |
}); | |
const PaymentMethodIsValid = await touchAndValidateForm( | |
PaymentMethodForm | |
) | |
.then(() => { | |
return true; | |
}) | |
.catch(() => { | |
window.dispatchMessages && | |
window.dispatchMessages( | |
[ | |
{ | |
type: "warning", | |
text: "No valid payment method selected", | |
}, | |
], | |
5000 | |
); | |
return false; | |
}); | |
const DeliveryInfoIsValid = await touchAndValidateForm(DeliveryInfoForm) | |
.then(() => { | |
return true; | |
}) | |
.catch(() => { | |
window.dispatchMessages && | |
window.dispatchMessages( | |
[ | |
{ | |
type: "warning", | |
text: "No Shipping Address Entered", | |
}, | |
], | |
5000 | |
); | |
return false; | |
}); | |
const ShippingMethodIsValid = await touchAndValidateForm( | |
ShippingMethodForm | |
) | |
.then(() => { | |
return true; | |
}) | |
.catch(() => { | |
window.dispatchMessages && | |
window.dispatchMessages( | |
[ | |
{ | |
type: "warning", | |
text: "No Shipping Method Selected", | |
}, | |
], | |
5000 | |
); | |
return false; | |
}); | |
const CheckoutAgreementsIsValid = await touchAndValidateForm( | |
CheckoutAgreementsForm | |
) | |
.then(() => { | |
return true; | |
}) | |
.catch(() => { | |
window.dispatchMessages && | |
window.dispatchMessages( | |
[ | |
{ | |
type: "warning", | |
text: "You need to accept the terms and agreements", | |
}, | |
], | |
5000 | |
); | |
return false; | |
}); | |
const ShippingAddressFormIsValid = await touchAndValidateForm( | |
ShippingAddressForm | |
) | |
.then(() => { | |
return true; | |
}).catch(() => { | |
window.dispatchMessages && | |
window.dispatchMessages( | |
[ | |
{ | |
type: "warning", | |
text: "Check your Shipping Address.", | |
}, | |
], | |
5000 | |
); | |
return false; | |
}); | |
/* Check if the Billing Address form is valid */ | |
const BillingAddressFormIsValid = await touchAndValidateForm( | |
BillingAddressForm | |
) | |
.then(() => { | |
/* If the Billing address is valid, use the billing address */ | |
return true; | |
}) | |
.catch(() => { | |
if (BillingAddressForm.values.same_as_shipping) { | |
/* If billing has not been set to open, set the shipping address as the billing address */ | |
BillingAddressForm.values = ShippingAddressForm.values; | |
return true; | |
} else if (config.storeViewCode !== "default") { | |
BillingAddressForm.values.same_as_shipping = true; | |
} else { | |
/* If the billing address has not been set to the same, and there is no billing address valid, send an error message */ | |
window.dispatchMessages && | |
window.dispatchMessages( | |
[ | |
{ | |
type: "warning", | |
text: "Check your Billing Address.", | |
}, | |
], | |
5000 | |
); | |
return false; | |
} | |
}); | |
/* PLACE ORDER START | |
- Check if all forms are valid before placing the order */ | |
if ( | |
EmailFormIsValid && | |
ShippingAddressFormIsValid && | |
BillingAddressFormIsValid && | |
PaymentMethodIsValid && | |
ShippingMethodIsValid && | |
CheckoutAgreementsIsValid && | |
DeliveryInfoIsValid | |
) { | |
/* If newsletter has been checked, subscribe the user to the newsletter when placing an order with the filled in email address */ | |
if (NewsLetterForm.values.newsletter === true) { | |
subscribeToNewsletter(EmailForm.values.email); | |
} | |
/* If create account has been checked, create the account, get new cart and merge them. */ | |
if ( | |
EmailForm.values.create_account && | |
EmailForm.values.create_account_password !== "" | |
) { | |
/* Create a new account */ | |
await createAccount( | |
EmailForm.values, | |
BillingAddressForm.values | |
); | |
/* Get the customer token for the account */ | |
const createdToken = await fetchCustomerToken( | |
EmailForm.values | |
).then((result) => result.generateCustomerToken.token); | |
if (createdToken) { | |
/* If there is a createdToken, save the token to the local storage for auth */ | |
LocalStorage.saveCustomerToken(createdToken); | |
/* Create a new cart and merge it with the existing one */ | |
const newCart = await createEmptyCart(); | |
await mergeCarts(newCart.createEmptyCart).then(handleLogin); | |
/* Save new cart to local storage */ | |
await LocalStorage.saveCartId(newCart.createEmptyCart); | |
/* see if we need to set different shipping address */ | |
BillingAddressForm.values.same_as_shipping | |
? await setShippingAddress({ | |
address: BillingAddressForm.values, | |
}) | |
: await setShippingAddress({ | |
address: ShippingAddressForm.values, | |
}); | |
// Set the shipping method explicitly for new cart | |
// Every change triggers a shipping method update, | |
// the cart isn't active after place order action. | |
await setShippingMethod( | |
ShippingMethodForm.values.carrier_code, | |
ShippingMethodForm.values.method_code | |
); | |
/* create the address when a new account has been created */ | |
ShippingAddressForm.values.save_in_address_book = true; | |
} | |
} | |
/* Place order and send all values */ | |
await placeOrder( | |
BillingAddressForm.values, | |
PaymentMethodForm.values, | |
ShippingMethodForm.values, | |
DeliveryInfoForm.values, | |
ShippingAddressForm.values.save_in_address_book | |
) | |
.then(() => setShowLoader(false)) | |
.catch((error) => { | |
console.error(error) | |
setShowLoader(false) | |
}) | |
.finally(() => setShowLoader(false)); | |
} else { | |
// Order not placed | |
setShowLoader(false); | |
if (btnRef.current) { | |
btnRef.current.removeAttribute("disabled"); | |
} | |
} | |
}; | |
return ( | |
<div className={"mb-4"}> | |
<div> | |
<button | |
type={"submit"} | |
disabled={!canPlaceOrder()} | |
className={ | |
"disabled:opacity-75 btn btn-add-to-cart px-3 py-3 w-full flex justify-center items-center text-xl font-extrabold" | |
} | |
onClick={submitHandler} | |
ref={btnRef} | |
> | |
<span>Plaats bestelling</span> | |
<ArrowDownIcon | |
className={`h-3 ml-1 transform -rotate-90`} | |
/> | |
</button> | |
</div> | |
{showLoader && <Loader />} | |
</div> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment