Yeah, since it seems like quite a few people are looking into this, i might as well just share the code that i use:
import {useState, useEffect} from 'react';
import {json, redirect} from '@shopify/remix-oxygen';
import {Form, useActionData} from '@remix-run/react';
// Define the page as public so it's accessible without authentication
export const handle = {
isPublic: true,
};
export async function action({request, context}) {
const {storefront, session, cart} = context;
const formData = await request.formData();
const firstName = formData.get('firstName');
const lastName = formData.get('lastName');
const email = formData.get('email');
const password = formData.get('password');
const passwordConfirm = formData.get('passwordConfirm');
const acceptsMarketing = formData.has('acceptsMarketing');
const phone = formData.get('phone') || undefined;
// Add a hidden field for account type
const accountType = 'consumer';
// Form validation
if (!email || !password || typeof email !== 'string' || typeof password !== 'string') {
return json({error: 'Please provide both an email and a password.'}, {status: 400});
}
if (password !== passwordConfirm) {
return json({error: 'Passwords do not match.'}, {status: 400});
}
try {
// We'll store the account type in metafields or tags
// For this example, let's use tags to identify account type
const tags = ['consumer'];
// Execute the customerCreate mutation
const data = await storefront.mutate(
CUSTOMER_CREATE_MUTATION,
{
variables: {
input: {
firstName,
lastName,
email,
password,
phone,
acceptsMarketing,
tags
},
},
}
);
// Check for errors from the mutation
if (data?.customerCreate?.customerUserErrors?.length) {
const error = data.customerCreate.customerUserErrors[0];
return json({error: error.message}, {status: 400});
}
if (!data?.customerCreate?.customer) {
return json({error: 'Something went wrong. Please try again.'}, {status: 500});
}
// After successful registration, automatically log in the user
const loginData = await storefront.mutate(
CUSTOMER_ACCESS_TOKEN_CREATE_MUTATION,
{
variables: {
input: {
email,
password,
},
},
}
);
if (loginData?.customerAccessTokenCreate?.customerUserErrors?.length) {
const error = loginData.customerAccessTokenCreate.customerUserErrors[0];
return json({error: error.message}, {status: 400});
}
const {customerAccessToken} = loginData.customerAccessTokenCreate;
if (!customerAccessToken?.accessToken) {
return json({error: 'Something went wrong. Please try again.'}, {status: 500});
}
// Store token in session
session.set('customerAccessToken', customerAccessToken.accessToken);
session.set('accountType', accountType);
if (customerAccessToken.expiresAt) {
session.set('customerAccessTokenExpiresAt', customerAccessToken.expiresAt);
}
// Initialize headers
let headers = new Headers();
try {
// Create a new cart with buyer identity
const result = await storefront.mutate(CART_CREATE_MUTATION, {
variables: {
input: {
buyerIdentity: {
customerAccessToken: customerAccessToken.accessToken,
},
},
},
});
if (result?.cartCreate?.cart?.id) {
// Set new cart ID in cookies
headers = cart.setCartId(result.cartCreate.cart.id);
}
} catch (cartError) {
console.error('Cart operation error:', cartError);
// Continue with registration even if cart operations fail
}
// Add session cookie
headers.append('Set-Cookie', await session.commit());
// Redirect to account page
return redirect('/', {headers});
} catch (error) {
console.error('Registration error:', error);
return json(
{error: 'An unexpected error occurred. Please try again.'},
{status: 500}
);
}
}
export default function ConsumerRegister() {
const actionData = useActionData();
const [isSubmitting, setIsSubmitting] = useState(false);
const [passwordsMatch, setPasswordsMatch] = useState(true);
// Reset isSubmitting state when actionData changes (means form submission completed)
useEffect(() => {
if (actionData) {
setIsSubmitting(false);
}
}, [actionData]);
const handlePasswordConfirmChange = (e) => {
const password = document.getElementById('password')?.value;
setPasswordsMatch(e.target.value === password);
};
const handleBackClick = () => {
window.location.href = '/register';
};
return (
<div className="flex min-h-full flex-col justify-center px-4 py-12 sm:px-6 lg:px-8 mt-[64px]">
<div className="mx-auto w-full max-w-md">
<div className="flex items-center mb-6">
<button
onClick={handleBackClick}
className="mr-4 text-gray-500 hover:text-gray-700"
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
</button>
<h1 className="text-center text-3xl font-bold tracking-tight text-gray-900 flex-grow">
Create Consumer Account
</h1>
</div>
{actionData?.error && (
<div className="mt-4 rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-red-400"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">{actionData.error}</h3>
</div>
</div>
</div>
)}
<div className="mt-8">
<div className="bg-white px-6 py-8 shadow sm:rounded-lg sm:px-10">
<Form
method="post"
noValidate
className="space-y-6"
onSubmit={() => setIsSubmitting(true)}
>
{/* Hidden field for registerType */}
<input type="hidden" name="registerType" value="consumer" />
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-gray-900">
First name
</label>
<div className="mt-2">
<input
id="firstName"
name="firstName"
type="text"
autoComplete="given-name"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3"
/>
</div>
</div>
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-gray-900">
Last name
</label>
<div className="mt-2">
<input
id="lastName"
name="lastName"
type="text"
autoComplete="family-name"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3"
/>
</div>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-900">
Email address
</label>
<div className="mt-2">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3"
/>
</div>
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-900">
Phone number (optional)
</label>
<div className="mt-2">
<input
id="phone"
name="phone"
type="tel"
autoComplete="tel"
placeholder="+15146669999"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3"
/>
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-900">
Password
</label>
<div className="mt-2">
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3"
/>
</div>
</div>
<div>
<label htmlFor="passwordConfirm" className="block text-sm font-medium text-gray-900">
Confirm password
</label>
<div className="mt-2">
<input
id="passwordConfirm"
name="passwordConfirm"
type="password"
autoComplete="new-password"
required
onChange={handlePasswordConfirmChange}
className={`block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ${
passwordsMatch ? 'ring-gray-300' : 'ring-red-300'
} placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3`}
/>
</div>
{!passwordsMatch && (
<p className="mt-1 text-sm text-red-600">Passwords do not match</p>
)}
</div>
<div className="flex items-center">
<input
id="acceptsMarketing"
name="acceptsMarketing"
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600"
/>
<label htmlFor="acceptsMarketing" className="ml-2 block text-sm text-gray-900">
Receive email updates and promotions
</label>
</div>
<div>
<button
type="submit"
disabled={isSubmitting || !passwordsMatch}
className="flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:bg-blue-300"
>
{isSubmitting ? 'Creating account...' : 'Create account'}
</button>
</div>
</Form>
</div>
</div>
</div>
</div>
);
}
// Customer Create Mutation
const CUSTOMER_CREATE_MUTATION = `#graphql
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
customer {
id
firstName
lastName
email
phone
acceptsMarketing
tags
}
customerUserErrors {
code
field
message
}
}
}
`;
// Customer Access Token Create Mutation (for automatic login after registration)
const CUSTOMER_ACCESS_TOKEN_CREATE_MUTATION = `#graphql
mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) {
customerAccessTokenCreate(input: $input) {
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`;
// Cart Create Mutation
const CART_CREATE_MUTATION = `#graphql
mutation cartCreate($input: CartInput!) {
cartCreate(input: $input) {
cart {
id
checkoutUrl
buyerIdentity {
customer {
id
email
firstName
lastName
displayName
}
}
}
userErrors {
field
message
}
}
}
`;
This is our custom hydrogen registration page, hopefully this can give you a better understanding.