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:
co-authored by
Claude Haiku 4.5
parent
0cc072fda4
commit
b7379e379b
@@ -5,6 +5,7 @@ import { prisma } from '@/lib/prisma';
|
|||||||
import { upsertMailingListEntry } from '@/lib/mailingList';
|
import { upsertMailingListEntry } from '@/lib/mailingList';
|
||||||
import { verifyTurnstileToken } from '@/lib/turnstile';
|
import { verifyTurnstileToken } from '@/lib/turnstile';
|
||||||
import { getClientIp } from '@/lib/rateLimit';
|
import { getClientIp } from '@/lib/rateLimit';
|
||||||
|
import { getCurrentCustomer } from '@/lib/auth';
|
||||||
|
|
||||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
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." };
|
return { ok: true, message: "Thanks — you're on the list." };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkCustomerAuth() {
|
||||||
|
const customer = await getCurrentCustomer();
|
||||||
|
return { isAuthenticated: !!customer };
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import type Konva from 'konva';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import ProductMockup from './ProductMockup';
|
import ProductMockup from './ProductMockup';
|
||||||
import Price from './Price';
|
import Price from './Price';
|
||||||
|
import LoginPromptModal from './LoginPromptModal';
|
||||||
import { useCart } from '@/lib/cartStore';
|
import { useCart } from '@/lib/cartStore';
|
||||||
import { getEffectivePrice } from '@/lib/pricing';
|
import { getEffectivePrice } from '@/lib/pricing';
|
||||||
|
import { checkCustomerAuth } from '@/app/actions';
|
||||||
import type { DesignElement, PrintArea, ProductDTO } from '@/lib/types';
|
import type { DesignElement, PrintArea, ProductDTO } from '@/lib/types';
|
||||||
|
|
||||||
const DISPLAY = 560; // css px size the mockup + stage are rendered at
|
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 [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const [quantity, setQuantity] = useState(1);
|
const [quantity, setQuantity] = useState(1);
|
||||||
const [justAdded, setJustAdded] = useState(false);
|
const [justAdded, setJustAdded] = useState(false);
|
||||||
|
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||||
const [nodeReadyTick, setNodeReadyTick] = useState(0);
|
const [nodeReadyTick, setNodeReadyTick] = useState(0);
|
||||||
|
|
||||||
const stageFrontRef = useRef<Konva.Stage | null>(null);
|
const stageFrontRef = useRef<Konva.Stage | null>(null);
|
||||||
@@ -462,7 +465,12 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
|
|||||||
).price
|
).price
|
||||||
: product.effectivePrice;
|
: product.effectivePrice;
|
||||||
|
|
||||||
const handleAddToBag = () => {
|
const handleAddToBag = async () => {
|
||||||
|
const { isAuthenticated } = await checkCustomerAuth();
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
setShowLoginPrompt(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (!canPersonalize) {
|
if (!canPersonalize) {
|
||||||
const flatPhoto = product.colorPhotos[color] ?? product.imageUrl ?? '';
|
const flatPhoto = product.colorPhotos[color] ?? product.imageUrl ?? '';
|
||||||
addItem({
|
addItem({
|
||||||
@@ -861,6 +869,8 @@ export default function DesignCanvas({ product }: { product: ProductDTO }) {
|
|||||||
</div>
|
</div>
|
||||||
{justAdded && <p className="text-sm text-pine">Added to your bag.</p>}
|
{justAdded && <p className="text-sm text-pine">Added to your bag.</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<LoginPromptModal isOpen={showLoginPrompt} onClose={() => setShowLoginPrompt(false)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,8 +4,10 @@ import { useState } from 'react';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import ProductMockup from './ProductMockup';
|
import ProductMockup from './ProductMockup';
|
||||||
import Price from './Price';
|
import Price from './Price';
|
||||||
|
import LoginPromptModal from './LoginPromptModal';
|
||||||
import { useCart } from '@/lib/cartStore';
|
import { useCart } from '@/lib/cartStore';
|
||||||
import { getColorName } from '@/lib/colorNames';
|
import { getColorName } from '@/lib/colorNames';
|
||||||
|
import { checkCustomerAuth } from '@/app/actions';
|
||||||
import type { ProductDTO } from '@/lib/types';
|
import type { ProductDTO } from '@/lib/types';
|
||||||
|
|
||||||
export default function StandardProductView({ product }: { product: ProductDTO }) {
|
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 [view, setView] = useState<'front' | 'back'>('front');
|
||||||
const [quantity, setQuantity] = useState(1);
|
const [quantity, setQuantity] = useState(1);
|
||||||
const [justAdded, setJustAdded] = useState(false);
|
const [justAdded, setJustAdded] = useState(false);
|
||||||
|
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||||
const addItem = useCart((s) => s.addItem);
|
const addItem = useCart((s) => s.addItem);
|
||||||
|
|
||||||
const hasBack = Boolean(product.imageUrlBack);
|
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" />;
|
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({
|
addItem({
|
||||||
cartItemId: uuid(),
|
cartItemId: uuid(),
|
||||||
productId: product.id,
|
productId: product.id,
|
||||||
@@ -182,6 +190,8 @@ export default function StandardProductView({ product }: { product: ProductDTO }
|
|||||||
</div>
|
</div>
|
||||||
{justAdded && <p className="text-sm text-pine">Added to your bag.</p>}
|
{justAdded && <p className="text-sm text-pine">Added to your bag.</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<LoginPromptModal isOpen={showLoginPrompt} onClose={() => setShowLoginPrompt(false)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user