Skip to content

Commit

Permalink
Add Mollie payment integration and enhance form validation
Browse files Browse the repository at this point in the history
  • Loading branch information
hreinberger committed Jan 16, 2025
1 parent 8b2f949 commit 3c33e89
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 78 deletions.
3 changes: 3 additions & 0 deletions src/app/components/form/address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ export default async function Address() {
<Select.Item value="AT">Austria</Select.Item>
<Select.Item value="NL">Netherlands</Select.Item>
<Select.Item value="UK">United Kingdom</Select.Item>
<Select.Item value="XI">
Northern Ireland (will error)
</Select.Item>
</Select.Content>
</Select.Root>
</Flex>
Expand Down
26 changes: 25 additions & 1 deletion src/app/components/form/checkoutbutton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { Button } from '@radix-ui/themes';
import { useFormStatus } from 'react-dom';
import { checkoutVariant } from '@/app/lib/validation';
import { useMollie } from '@/app/lib/MollieContext';
import { createPayment } from '@/app/lib/server-actions';

export default function CheckoutButton({
variant,
Expand All @@ -12,14 +14,36 @@ export default function CheckoutButton({
// display a loading indicator once the button is clicked
const { pending } = useFormStatus();

// handle form submission when the card component is used
// here we have to first get the card token from mollie
// then we have to append the token to the form and submit it to the createPayment function

const { mollie } = useMollie();
const payWithToken = async () => {
const formElement = document.querySelector('form');
if (!formElement) {
console.error('Form element not found');
return;
}
const formData = new FormData(formElement);
const { token, error } = await mollie.createToken();
if (error) {
console.error('Error creating card token:', error);
return;
}
formData.append('cardToken', token);
createPayment(formData);
};

return (
<Button
variant="solid"
size="3"
className="w-8/12 sm:w-6/12 lg:w-4/12"
loading={pending}
formAction={variant === 'components' ? payWithToken : createPayment}
>
Buy Now
Buy Now ({variant})
</Button>
);
}
3 changes: 1 addition & 2 deletions src/app/components/form/checkoutform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Flex, Grid, Heading } from '@radix-ui/themes';
import React, { useState } from 'react';

// Lib
import { createPayment } from '@/app/lib/server-actions';
import { checkoutVariant } from '@/app/lib/validation';

// Form components
Expand All @@ -28,7 +27,7 @@ export default function CheckoutForm({
React.useState<checkoutVariant>('hosted');
return (
// The form data is sent to the createPayment function when the form is submitted
<form action={createPayment}>
<form>
<Flex
direction="column"
m="6"
Expand Down
52 changes: 12 additions & 40 deletions src/app/components/form/methods/componentpaymentmethods.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
'use client';

declare global {
interface Window {
Mollie: any;
}
}

import {
Card,
Flex,
Expand All @@ -18,48 +12,26 @@ import {
Box,
} from '@radix-ui/themes';

import React, { Suspense, useEffect, useRef } from 'react';
import React, { Suspense, useEffect, useRef, useState } from 'react';
import { useMollie } from '@/app/lib/MollieContext';
import { IdCardIcon } from '@radix-ui/react-icons';
import PaymentLogo from '@/app/components/form/paymentlogo';

export default function ComponentPaymentMethods() {
const mollieInitialized = useRef(false);
const mollieObject = useRef<any>(null);
const mollieComponents = useRef<any>(null);

// Load Mollie script and initialize Mollie object
const { mollie } = useMollie();
useEffect(() => {
let mollie = window.Mollie('pfl_FHTbr2nyYb', {
locale: 'en_US',
testmode: true,
});
console.debug('Mollie object created');
console.debug(mollie);
mollieInitialized.current = true;
mollieObject.current = mollie;

if (mollieInitialized.current) {
const card = document.getElementById('card');
if (card) {
console.debug('Creating Mollie card component');
console.debug(mollieObject.current);
const mollie = mollieObject.current;
const cardComponent = mollie.createComponent('card');
cardComponent.mount(card);
mollieComponents.current = cardComponent;
}
let cardComponent: any;
if (mollie) {
cardComponent = mollie.createComponent('card');
cardComponent.mount('#card');
}

return () => {
console.debug('Unmounting Mollie card component');
const cardComponent = mollieComponents.current;
cardComponent.unmount();
mollie = null;
mollieInitialized.current = false;
mollieObject.current = null;
mollieComponents.current = null;
if (cardComponent) {
cardComponent.unmount();
}
};
}, [mollieInitialized, mollieObject, mollieComponents]);
}, [mollie]);

return (
<>
Expand Down Expand Up @@ -113,7 +85,7 @@ export default function ComponentPaymentMethods() {
<IdCardIcon />
</Callout.Icon>
<Callout.Text>
<Code variant="soft">2223 0000 1047 9399</Code>
<Code variant="ghost">2223 0000 1047 9399</Code>
</Callout.Text>
</Callout.Root>
</Flex>
Expand Down
68 changes: 36 additions & 32 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import Footer from '@/app/components/ui/footer.js';
import { Providers } from '@/app/components/ui/providers.jsx';
import Script from 'next/script';

import { MollieProvider } from './lib/MollieContext';

export const metadata: Metadata = {
title: 'Mollie Demo App',
description: 'A demo app for Mollie payments, written in nextJS',
Expand All @@ -23,40 +25,42 @@ export default function RootLayout({
suppressHydrationWarning
>
<body>
<Providers>
<Theme
accentColor="blue"
grayColor="gray"
panelBackground="solid"
scaling="110%"
radius="large"
>
{/* <ThemePanel /> */}
<Container size="4">
<Section
pt="0"
pb="4"
>
<Navbar />
</Section>
<Section
pt="4"
pb="4"
>
{children}
</Section>
<Section
pt="4"
pb="0"
>
<Footer />
</Section>
</Container>
</Theme>
</Providers>
<MollieProvider>
<Providers>
<Theme
accentColor="blue"
grayColor="gray"
panelBackground="solid"
scaling="110%"
radius="large"
>
{/* <ThemePanel /> */}
<Container size="4">
<Section
pt="0"
pb="4"
>
<Navbar />
</Section>
<Section
pt="4"
pb="4"
>
{children}
</Section>
<Section
pt="4"
pb="0"
>
<Footer />
</Section>
</Container>
</Theme>
</Providers>
</MollieProvider>
<Script
src="https://js.mollie.com/v1/mollie.js"
strategy="lazyOnload"
strategy="beforeInteractive"
/>
</body>
</html>
Expand Down
49 changes: 49 additions & 0 deletions src/app/lib/MollieContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use client';

import React, {
createContext,
useContext,
useRef,
useState,
useEffect,
} from 'react';

export const MollieContext = createContext();

export const MollieProvider = ({ children }) => {
const mollieRef = useRef(null);
const [mollie, setMollie] = useState(null);

useEffect(() => {
// Load Mollie script and initialize
const loadMollie = async () => {
if (!mollieRef.current) {
mollieRef.current = Mollie('pfl_FHTbr2nyYb', {
locale: 'en_US',
testmode: true,
});
setMollie(mollieRef.current);
}
};

loadMollie();

// Cleanup function to unload Mollie object
return () => {
if (mollieRef.current) {
mollieRef.current = null;
setMollie(null);
}
};
}, []);

return (
<MollieContext.Provider value={{ mollie }}>
{children}
</MollieContext.Provider>
);
};

export const useMollie = () => {
return useContext(MollieContext);
};
3 changes: 3 additions & 0 deletions src/app/lib/mollie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export async function mollieCreatePayment({
zip_code,
country,
payment_method,
cardToken,
}: {
firstname: string;
lastname: string;
Expand All @@ -37,6 +38,7 @@ export async function mollieCreatePayment({
zip_code: string;
country: string;
payment_method: string | undefined;
cardToken?: string;
}) {
// we need to construct the billingAdress object first as long as this isn't fixed:
// https://github.com/mollie/mollie-api-node/issues/390#issuecomment-2467604847
Expand Down Expand Up @@ -107,6 +109,7 @@ export async function mollieCreatePayment({
webhookUrl: webhookUrl,
method: payment_method as undefined, // undefined for now
...{ billingAddress },
cardToken: cardToken,
});
const redirectUrl = payment.getCheckoutUrl();
return redirectUrl;
Expand Down
3 changes: 1 addition & 2 deletions src/app/lib/server-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { redirect } from 'next/navigation';

export async function createPayment(formData: FormData) {
// This Server Action takes the form data, validates it and creates a payment
// The 'use server' pragma is used to indicate that this function should be run on the server

// Always validate user input
const validatedForm: {
firstname: string;
Expand All @@ -22,6 +20,7 @@ export async function createPayment(formData: FormData) {
zip_code: string;
country: string;
payment_method: string | undefined;
cardToken?: string;
} = await validateFormData(formData);

// Create a payment with the validated form data and retrieve the redirect URL
Expand Down
1 change: 1 addition & 0 deletions src/app/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export async function validateFormData(formData: FormData) {
.min(1, { message: 'Must be at least 1 character long.' }),
country: z.string().length(2),
payment_method: z.string(),
cardToken: z.string().startsWith('tkn_').optional(),
});

try {
Expand Down
8 changes: 7 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
},
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"src/app/lib/MollieContext.js"
],
"exclude": ["node_modules"]
}

0 comments on commit 3c33e89

Please sign in to comment.