Add login prompt when non-authenticated users add items to cart

- Create LoginPromptModal component for authentication encouragement
- Check customer authentication before adding items to cart
- Show modal for non-logged-in users with options to create account, log in, or continue as guest
- Allow guests to add items (stored in IndexedDB) while prompting to sign up
- Implemented in both StandardProductView (ready-made products) and DesignCanvas (personalized items)
- Add server action checkCustomerAuth() for authentication checks

Addresses user request: prompt guests to create account or log in when adding items to cart.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-17 22:53:36 +01:00
co-authored by Claude Haiku 4.5
parent 0cc072fda4
commit b7379e379b
4 changed files with 75 additions and 2 deletions
+6
View File
@@ -5,6 +5,7 @@ import { prisma } from '@/lib/prisma';
import { upsertMailingListEntry } from '@/lib/mailingList';
import { verifyTurnstileToken } from '@/lib/turnstile';
import { getClientIp } from '@/lib/rateLimit';
import { getCurrentCustomer } from '@/lib/auth';
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -30,3 +31,8 @@ export async function subscribeToNewsletter(email: string, turnstileToken: strin
return { ok: true, message: "Thanks — you're on the list." };
}
export async function checkCustomerAuth() {
const customer = await getCurrentCustomer();
return { isAuthenticated: !!customer };
}
+11 -1
View File
@@ -6,8 +6,10 @@ import type Konva from 'konva';
import { v4 as uuid } from 'uuid';
import ProductMockup from './ProductMockup';
import Price from './Price';
import LoginPromptModal from './LoginPromptModal';
import { useCart } from '@/lib/cartStore';
import { getEffectivePrice } from '@/lib/pricing';
import { checkCustomerAuth } from '@/app/actions';
import type { DesignElement, PrintArea, ProductDTO } from '@/lib/types';
const DISPLAY = 560; // css px size the mockup + stage are rendered at
@@ -310,6 +312,7 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
const [quantity, setQuantity] = useState(1);
const [justAdded, setJustAdded] = useState(false);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const [nodeReadyTick, setNodeReadyTick] = useState(0);
const stageFrontRef = useRef<Konva.Stage | null>(null);
@@ -462,7 +465,12 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
).price
: product.effectivePrice;
const handleAddToBag = () => {
const handleAddToBag = async () => {
const { isAuthenticated } = await checkCustomerAuth();
if (!isAuthenticated) {
setShowLoginPrompt(true);
}
if (!canPersonalize) {
const flatPhoto = product.colorPhotos[color] ?? product.imageUrl ?? '';
addItem({
@@ -861,6 +869,8 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
</div>
{justAdded && <p className="text-sm text-pine">Added to your bag.</p>}
</div>
<LoginPromptModal isOpen={showLoginPrompt} onClose={() => setShowLoginPrompt(false)} />
</div>
);
}
+47
View File
@@ -0,0 +1,47 @@
'use client';
import Link from 'next/link';
interface LoginPromptModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function LoginPromptModal({ isOpen, onClose }: LoginPromptModalProps) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="mx-4 w-full max-w-sm rounded-lg border border-line bg-paper p-6 shadow-lg">
<h2 className="font-display text-2xl">Create an account to shop</h2>
<p className="mt-3 text-sm text-muted">
Add items to your bag and save your designs. You'll need an account to checkout.
</p>
<div className="mt-6 space-y-3">
<Link
href="/account/register"
onClick={onClose}
className="block w-full rounded bg-clay px-4 py-3 text-center text-sm font-medium text-paper transition-colors hover:bg-clay-dark"
>
Create account
</Link>
<Link
href="/account/login"
onClick={onClose}
className="block w-full rounded border border-ink px-4 py-3 text-center text-sm font-medium text-ink transition-colors hover:border-clay hover:bg-clay hover:text-paper"
>
Log in
</Link>
</div>
<button
onClick={onClose}
className="mt-4 w-full text-center text-sm text-muted hover:text-ink"
>
Continue as guest
</button>
</div>
</div>
);
}
+11 -1
View File
@@ -4,8 +4,10 @@ import { useState } from 'react';
import { v4 as uuid } from 'uuid';
import ProductMockup from './ProductMockup';
import Price from './Price';
import LoginPromptModal from './LoginPromptModal';
import { useCart } from '@/lib/cartStore';
import { getColorName } from '@/lib/colorNames';
import { checkCustomerAuth } from '@/app/actions';
import type { ProductDTO } from '@/lib/types';
export default function StandardProductView({ product }: { product: ProductDTO }) {
@@ -14,6 +16,7 @@ export default function StandardProductView({ product }: { product: ProductDTO }
const [view, setView] = useState<'front' | 'back'>('front');
const [quantity, setQuantity] = useState(1);
const [justAdded, setJustAdded] = useState(false);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const addItem = useCart((s) => s.addItem);
const hasBack = Boolean(product.imageUrlBack);
@@ -44,7 +47,12 @@ export default function StandardProductView({ product }: { product: ProductDTO }
return <img src={activePhoto} alt={product.name} className="h-full w-full object-cover" />;
}
const handleAddToBag = () => {
const handleAddToBag = async () => {
const { isAuthenticated } = await checkCustomerAuth();
if (!isAuthenticated) {
setShowLoginPrompt(true);
}
addItem({
cartItemId: uuid(),
productId: product.id,
@@ -182,6 +190,8 @@ export default function StandardProductView({ product }: { product: ProductDTO }
</div>
{justAdded && <p className="text-sm text-pine">Added to your bag.</p>}
</div>
<LoginPromptModal isOpen={showLoginPrompt} onClose={() => setShowLoginPrompt(false)} />
</div>
);
}