From 22a19e1b5e8d0fec5c50e26bde2853fb6eaffc47 Mon Sep 17 00:00:00 2001 From: Damian Legawiec Date: Sun, 15 Mar 2026 20:24:37 +0100 Subject: [PATCH 1/2] Implement reset password --- package-lock.json | 18 +- package.json | 4 +- .../account/forgot-password/page.tsx | 155 +++++++++ .../[locale]/(storefront)/account/layout.tsx | 5 +- .../[locale]/(storefront)/account/page.tsx | 303 +++++++++--------- .../(storefront)/account/register/page.tsx | 276 ++++++++-------- .../account/reset-password/page.tsx | 240 ++++++++++++++ src/components/ui/card.tsx | 75 +++++ src/lib/data/customer.ts | 17 + 9 files changed, 808 insertions(+), 285 deletions(-) create mode 100644 src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx create mode 100644 src/app/[country]/[locale]/(storefront)/account/reset-password/page.tsx create mode 100644 src/components/ui/card.tsx diff --git a/package-lock.json b/package-lock.json index 2721fdd1..0a1c19c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "dependencies": { "@next/third-parties": "^16.1.6", "@sentry/nextjs": "^10.38.0", - "@spree/next": "^0.10.4", - "@spree/sdk": "^0.10.0", + "@spree/next": "^0.10.5", + "@spree/sdk": "^0.10.1", "@stripe/react-stripe-js": "^5.6.0", "@stripe/stripe-js": "^8.7.0", "class-variance-authority": "^0.7.1", @@ -5485,20 +5485,20 @@ } }, "node_modules/@spree/next": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@spree/next/-/next-0.10.4.tgz", - "integrity": "sha512-O2WED5CSPrt9XKzzsWmJ1mPlusLFmi1/SvEKUeMV3hEzfTRaaRBW2U0Q6ZjFtqV36zHVVgnwaUkXw6lXeKRQPg==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@spree/next/-/next-0.10.5.tgz", + "integrity": "sha512-+D3TST5K2iogTGJKoCGcP2aw5wgMkhorvS+bkeYlToMyPuhSGRYSyK8CFZN8Ctt5Uhc+HqYhNBbZi3cYXGYn8w==", "license": "MIT", "peerDependencies": { - "@spree/sdk": ">=0.10.0", + "@spree/sdk": ">=0.10.1", "next": ">=15.0.0", "react": ">=19.0.0" } }, "node_modules/@spree/sdk": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@spree/sdk/-/sdk-0.10.0.tgz", - "integrity": "sha512-2JZHy7EhBkW4xvTU5Z+btNq22s7yK41O+TBX1ng6TOmeL61S4riY5xneWVSyCEsDInSoUVnU1hp+V2Q0HmndHQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@spree/sdk/-/sdk-0.10.1.tgz", + "integrity": "sha512-TZd4zSy44htGo6KIR1kZR4G45TfRN1Yi3eNmqfsc1+xzQ7y1OFFoR1VmWhiTvZ+MO7NKGrtZv9nxJJaXw1mzHA==", "license": "MIT", "engines": { "node": ">=18.0.0" diff --git a/package.json b/package.json index e4418350..38c843bb 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "dependencies": { "@next/third-parties": "^16.1.6", "@sentry/nextjs": "^10.38.0", - "@spree/next": "^0.10.4", - "@spree/sdk": "^0.10.0", + "@spree/next": "^0.10.5", + "@spree/sdk": "^0.10.1", "@stripe/react-stripe-js": "^5.6.0", "@stripe/stripe-js": "^8.7.0", "class-variance-authority": "^0.7.1", diff --git a/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx b/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx new file mode 100644 index 00000000..2ec00343 --- /dev/null +++ b/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { CircleAlert, CircleCheck, Mail } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useState } from "react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Field, FieldLabel } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { requestPasswordReset } from "@/lib/data/customer"; +import { extractBasePath } from "@/lib/utils/path"; + +export default function ForgotPasswordPage() { + const pathname = usePathname(); + const basePath = extractBasePath(pathname); + + const [email, setEmail] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [submitted, setSubmitted] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSubmitting(true); + + try { + const origin = window.location.origin; + const redirectUrl = `${origin}${basePath}/account/reset-password`; + await requestPasswordReset(email, redirectUrl); + setSubmitted(true); + } catch { + setError("Something went wrong. Please try again."); + } finally { + setSubmitting(false); + } + }; + + if (submitted) { + return ( +
+ + +
+ +
+ Check your email + + If an account exists for {email}, we've sent + password reset instructions. + +
+ + +
+ +

+ The link in the email will expire in 15 minutes. Check your spam + folder if you don't see it. +

+
+ + +
+ + + + Back to sign in + + +
+
+ ); + } + + return ( +
+ + + Reset your password + + Enter your email and we'll send you a link to reset your + password. + + + + +
+ {error && ( + + + {error} + + )} + + + Email + setEmail(e.target.value)} + required + placeholder="you@example.com" + /> + + +
+ +
+
+
+ + + + Back to sign in + + +
+
+ ); +} diff --git a/src/app/[country]/[locale]/(storefront)/account/layout.tsx b/src/app/[country]/[locale]/(storefront)/account/layout.tsx index e213ff9a..e8b45113 100644 --- a/src/app/[country]/[locale]/(storefront)/account/layout.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/layout.tsx @@ -149,7 +149,10 @@ export default function AccountLayout({ const { user, logout, isAuthenticated, loading } = useAuth(); // Pages that don't require authentication - const isAuthPage = pathname.includes("/register"); + const isAuthPage = + pathname.includes("/register") || + pathname.includes("/forgot-password") || + pathname.includes("/reset-password"); const isMainAccountPage = pathname === `${basePath}/account`; // Redirect to login if not authenticated and trying to access protected sub-pages diff --git a/src/app/[country]/[locale]/(storefront)/account/page.tsx b/src/app/[country]/[locale]/(storefront)/account/page.tsx index 63530c7c..d051686d 100644 --- a/src/app/[country]/[locale]/(storefront)/account/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/page.tsx @@ -14,6 +14,14 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Field, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { useAuth } from "@/contexts/AuthContext"; @@ -69,92 +77,105 @@ export default function AccountPage() { if (!isAuthenticated) { return (
-
-

My Account

-

- Sign in to access your account and order history. -

-
+ + + My Account + + Sign in to access your account and order history. + + + + +
+ {error && ( + + + {error} + + )} -
- - {error && ( - - - {error} - - )} - - - Email - setEmail(e.target.value)} - required - placeholder="you@example.com" - /> - - - - Password -
+ + Email setPassword(e.target.value)} + type="email" + id="email" + name="email" + autoComplete="email" + value={email} + onChange={(e) => setEmail(e.target.value)} required - placeholder="••••••••" - className="pr-10" + placeholder="you@example.com" /> -
- + + + + Password +
+ setPassword(e.target.value)} + required + placeholder="••••••••" + className="pr-10" + /> +
+ +
+
+ +
+ + Forgot password? +
- - -
- +
+ + + + +

+ Don't have an account?{" "} + - {loading ? "Signing in..." : "Sign In"} - -

- - -
- Don't have an account? - - Sign up - -
-
+ Sign up + +

+ +
); } @@ -167,76 +188,72 @@ export default function AccountPage() {
- -
-
- -
-
-

- Order History -

-

- View your past orders and track shipments -

-
-
+ + + +
+ +
+
+

+ Order History +

+

+ View your past orders and track shipments +

+
+
+
- -
-
- -
-
-

Addresses

-

- Manage your shipping and billing addresses -

-
-
+ + + +
+ +
+
+

Addresses

+

+ Manage your shipping and billing addresses +

+
+
+
- -
-
- -
-
-

- Payment Methods -

-

- Manage your saved credit cards -

-
-
+ + + +
+ +
+
+

+ Payment Methods +

+

+ Manage your saved credit cards +

+
+
+
- -
-
- -
-
-

Profile

-

- Update your personal information -

-
-
+ + + +
+ +
+
+

Profile

+

+ Update your personal information +

+
+
+
diff --git a/src/app/[country]/[locale]/(storefront)/account/register/page.tsx b/src/app/[country]/[locale]/(storefront)/account/register/page.tsx index 99547a4a..5e618df6 100644 --- a/src/app/[country]/[locale]/(storefront)/account/register/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/register/page.tsx @@ -6,6 +6,14 @@ import { usePathname, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Field, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { useAuth } from "@/contexts/AuthContext"; @@ -77,148 +85,156 @@ export default function RegisterPage() { return (
-
-

Create Account

-

Sign up to start shopping with us.

-
- -
-
- {error && ( - - - {error} - - )} - -
+ + + Create Account + Sign up to start shopping with us. + + + + + {error && ( + + + {error} + + )} + +
+ + First name + setFirstName(e.target.value)} + required + placeholder="John" + /> + + + + Last name + setLastName(e.target.value)} + required + placeholder="Doe" + /> + +
+ - First name + Email setFirstName(e.target.value)} + type="email" + id="email" + value={email} + onChange={(e) => setEmail(e.target.value)} required - placeholder="John" + placeholder="you@example.com" /> - Last name - setLastName(e.target.value)} - required - placeholder="Doe" - /> - -
- - - Email - setEmail(e.target.value)} - required - placeholder="you@example.com" - /> - - - - Password -
- setPassword(e.target.value)} - required - minLength={6} - placeholder="••••••••" - className="pr-10" - /> -
- + Password +
+ setPassword(e.target.value)} + required + minLength={6} + placeholder="••••••••" + className="pr-10" + /> +
+ +
-
- + - - - Confirm Password - -
- setPasswordConfirmation(e.target.value)} - required - minLength={6} - placeholder="••••••••" - className="pr-10" - /> -
- + + + Confirm Password + +
+ setPasswordConfirmation(e.target.value)} + required + minLength={6} + placeholder="••••••••" + className="pr-10" + /> +
+ +
+
+ +
+
- - -
- -
- - -
- Already have an account? - - Sign in - -
-
+ Sign in + +

+ +
); } diff --git a/src/app/[country]/[locale]/(storefront)/account/reset-password/page.tsx b/src/app/[country]/[locale]/(storefront)/account/reset-password/page.tsx new file mode 100644 index 00000000..2197096b --- /dev/null +++ b/src/app/[country]/[locale]/(storefront)/account/reset-password/page.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { CircleAlert, CircleCheck, Eye, EyeOff } from "lucide-react"; +import Link from "next/link"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useState } from "react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Field, FieldLabel } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { resetPassword } from "@/lib/data/customer"; +import { extractBasePath } from "@/lib/utils/path"; + +export default function ResetPasswordPage() { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const basePath = extractBasePath(pathname); + const token = searchParams.get("token"); + + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [passwordConfirmation, setPasswordConfirmation] = useState(""); + const [showPasswordConfirmation, setShowPasswordConfirmation] = + useState(false); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + // No token = invalid link + if (!token) { + return ( +
+ + + Invalid link + + This password reset link is invalid or has expired. + + + + + + Request a new reset link + + + +
+ ); + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (password !== passwordConfirmation) { + setError("Passwords do not match"); + return; + } + + if (password.length < 6) { + setError("Password must be at least 6 characters"); + return; + } + + setSubmitting(true); + + try { + const result = await resetPassword(token, password, passwordConfirmation); + if (result.success) { + setSuccess(true); + } else { + setError( + result.error || + "This reset link has expired. Please request a new one.", + ); + } + } catch { + setError("Something went wrong. Please try again."); + } finally { + setSubmitting(false); + } + }; + + if (success) { + return ( +
+ + +
+ +
+ Password reset successful + + Your password has been updated. You can now sign in with your new + password. + +
+ + + + +
+
+ ); + } + + return ( +
+ + + Set a new password + Enter your new password below. + + + +
+ {error && ( + + + {error} + + )} + + + New password +
+ setPassword(e.target.value)} + required + minLength={6} + placeholder="••••••••" + className="pr-10" + /> +
+ +
+
+
+ + + + Confirm new password + +
+ setPasswordConfirmation(e.target.value)} + required + minLength={6} + placeholder="••••••••" + className="pr-10" + /> +
+ +
+
+
+ +
+ +
+
+
+ + + + Back to sign in + + +
+
+ ); +} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 00000000..d77f21aa --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,75 @@ +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +}; diff --git a/src/lib/data/customer.ts b/src/lib/data/customer.ts index 0e61391d..8772d72f 100644 --- a/src/lib/data/customer.ts +++ b/src/lib/data/customer.ts @@ -5,6 +5,8 @@ import { login as _login, logout as _logout, register as _register, + requestPasswordReset as _requestPasswordReset, + resetPassword as _resetPassword, updateCustomer as _updateCustomer, } from "@spree/next"; import { actionResult } from "./utils"; @@ -31,6 +33,21 @@ export async function logout() { return _logout(); } +export async function requestPasswordReset( + email: string, + redirectUrl?: string, +) { + return _requestPasswordReset(email, redirectUrl); +} + +export async function resetPassword( + token: string, + password: string, + passwordConfirmation: string, +) { + return _resetPassword(token, password, passwordConfirmation); +} + export async function updateCustomer(data: { first_name?: string; last_name?: string; From f0e12ba3b8bf3d723f1db4b8ac7ddd9d8b4b3b4c Mon Sep 17 00:00:00 2001 From: Damian Legawiec Date: Sun, 15 Mar 2026 20:44:16 +0100 Subject: [PATCH 2/2] CR fixes --- .../(storefront)/account/forgot-password/page.tsx | 8 ++++++-- .../[country]/[locale]/(storefront)/account/layout.tsx | 10 ++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx b/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx index 2ec00343..0a601d04 100644 --- a/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/forgot-password/page.tsx @@ -36,8 +36,12 @@ export default function ForgotPasswordPage() { try { const origin = window.location.origin; const redirectUrl = `${origin}${basePath}/account/reset-password`; - await requestPasswordReset(email, redirectUrl); - setSubmitted(true); + const result = await requestPasswordReset(email, redirectUrl); + if (result?.message) { + setSubmitted(true); + } else { + setError("Something went wrong. Please try again."); + } } catch { setError("Something went wrong. Please try again."); } finally { diff --git a/src/app/[country]/[locale]/(storefront)/account/layout.tsx b/src/app/[country]/[locale]/(storefront)/account/layout.tsx index e8b45113..1733437e 100644 --- a/src/app/[country]/[locale]/(storefront)/account/layout.tsx +++ b/src/app/[country]/[locale]/(storefront)/account/layout.tsx @@ -149,10 +149,12 @@ export default function AccountLayout({ const { user, logout, isAuthenticated, loading } = useAuth(); // Pages that don't require authentication - const isAuthPage = - pathname.includes("/register") || - pathname.includes("/forgot-password") || - pathname.includes("/reset-password"); + const authPagePaths = new Set([ + `${basePath}/account/register`, + `${basePath}/account/forgot-password`, + `${basePath}/account/reset-password`, + ]); + const isAuthPage = authPagePaths.has(pathname); const isMainAccountPage = pathname === `${basePath}/account`; // Redirect to login if not authenticated and trying to access protected sub-pages