Skip to content

Instantly share code, notes, and snippets.

@ArjenMiedema
Created December 9, 2021 12:48
Show Gist options
  • Save ArjenMiedema/95b08f5a86c959a34fc9a8a524bdb98a to your computer and use it in GitHub Desktop.
Save ArjenMiedema/95b08f5a86c959a34fc9a8a524bdb98a to your computer and use it in GitHub Desktop.
Mollie GraphQL implementation for Hyva Checkout
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;
}
};
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;
}
};
export const createMollieQuery = (paymentToken, issuer) => `mutation {
createMollieTransaction(input: {
payment_token: "${paymentToken}"
issuer: "${issuer}"
}) {
checkout_url
}
}`;
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>
);
};
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);
};
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