diff --git a/customer-spa/src/App.tsx b/customer-spa/src/App.tsx index 9ce4fea..591da3b 100644 --- a/customer-spa/src/App.tsx +++ b/customer-spa/src/App.tsx @@ -17,6 +17,7 @@ import Account from './pages/Account'; import Wishlist from './pages/Wishlist'; import Login from './pages/Login'; import ForgotPassword from './pages/ForgotPassword'; +import ResetPassword from './pages/ResetPassword'; // Create QueryClient instance const queryClient = new QueryClient({ @@ -89,6 +90,7 @@ function AppRoutes() { {/* Login & Auth */} } /> } /> + } /> {/* My Account */} } /> diff --git a/customer-spa/src/pages/ResetPassword/index.tsx b/customer-spa/src/pages/ResetPassword/index.tsx new file mode 100644 index 0000000..0847b8c --- /dev/null +++ b/customer-spa/src/pages/ResetPassword/index.tsx @@ -0,0 +1,300 @@ +import React, { useState, useEffect } from 'react'; +import { Link, useSearchParams, useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import Container from '@/components/Layout/Container'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { KeyRound, ArrowLeft, Eye, EyeOff, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'; + +export default function ResetPassword() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const key = searchParams.get('key') || ''; + const login = searchParams.get('login') || ''; + + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isValidating, setIsValidating] = useState(true); + const [isValid, setIsValid] = useState(false); + const [error, setError] = useState(''); + const [isSuccess, setIsSuccess] = useState(false); + + // Validate the reset key on mount + useEffect(() => { + const validateKey = async () => { + if (!key || !login) { + setError('Invalid password reset link. Please request a new one.'); + setIsValidating(false); + return; + } + + try { + const apiRoot = (window as any).woonoowCustomer?.apiRoot || '/wp-json/woonoow/v1'; + const nonce = (window as any).woonoowCustomer?.nonce || ''; + + const response = await fetch(`${apiRoot}/auth/validate-reset-key`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': nonce, + }, + credentials: 'include', + body: JSON.stringify({ key, login }), + }); + + const data = await response.json(); + + if (response.ok && data.valid) { + setIsValid(true); + } else { + setError(data.message || 'This password reset link has expired or is invalid.'); + } + } catch (err) { + setError('Unable to validate reset link. Please try again later.'); + } finally { + setIsValidating(false); + } + }; + + validateKey(); + }, [key, login]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + if (password !== confirmPassword) { + setError('Passwords do not match'); + return; + } + + if (password.length < 8) { + setError('Password must be at least 8 characters long'); + return; + } + + setIsLoading(true); + + try { + const apiRoot = (window as any).woonoowCustomer?.apiRoot || '/wp-json/woonoow/v1'; + const nonce = (window as any).woonoowCustomer?.nonce || ''; + + const response = await fetch(`${apiRoot}/auth/reset-password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': nonce, + }, + credentials: 'include', + body: JSON.stringify({ key, login, password }), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + setIsSuccess(true); + toast.success('Password reset successfully!'); + } else { + setError(data.message || 'Failed to reset password. Please try again.'); + } + } catch (err: any) { + setError(err.message || 'An error occurred. Please try again later.'); + } finally { + setIsLoading(false); + } + }; + + // Password strength indicator + const getPasswordStrength = (pwd: string) => { + if (pwd.length === 0) return { label: '', color: '', width: '0%' }; + if (pwd.length < 8) return { label: 'Too short', color: 'bg-red-500', width: '25%' }; + + let strength = 0; + if (pwd.length >= 8) strength++; + if (pwd.length >= 12) strength++; + if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) strength++; + if (/\d/.test(pwd)) strength++; + if (/[!@#$%^&*(),.?":{}|<>]/.test(pwd)) strength++; + + if (strength <= 2) return { label: 'Weak', color: 'bg-orange-500', width: '50%' }; + if (strength <= 3) return { label: 'Medium', color: 'bg-yellow-500', width: '75%' }; + return { label: 'Strong', color: 'bg-green-500', width: '100%' }; + }; + + const passwordStrength = getPasswordStrength(password); + + // Loading state + if (isValidating) { + return ( + +
+
+
+ +

Validating reset link...

+
+
+
+
+ ); + } + + // Success state + if (isSuccess) { + return ( + +
+
+
+
+ +
+

Password Reset!

+

+ Your password has been successfully updated. You can now log in with your new password. +

+ + + +
+
+
+
+ ); + } + + // Error state (invalid key) + if (!isValid && error) { + return ( + +
+
+
+
+ +
+

Invalid Reset Link

+

{error}

+ + + +
+
+
+
+ ); + } + + // Reset form + return ( + +
+
+ {/* Back link */} + + + Back to login + + +
+ {/* Header */} +
+
+ +
+

Reset Your Password

+

+ Enter your new password below. +

+
+ + {/* Error message */} + {error && ( +
+ {error} +
+ )} + + {/* Form */} +
+
+ +
+ setPassword(e.target.value)} + placeholder="Enter new password" + required + disabled={isLoading} + className="pr-10" + /> + +
+ {password && ( +
+
+
+
+

+ Strength: {passwordStrength.label} +

+
+ )} +
+ +
+ + setConfirmPassword(e.target.value)} + placeholder="Confirm new password" + required + disabled={isLoading} + /> + {confirmPassword && password !== confirmPassword && ( +

Passwords do not match

+ )} +
+ + + + + {/* Footer */} +
+ Remember your password?{' '} + + Sign in + +
+
+
+
+ + ); +} diff --git a/includes/Core/Notifications/EmailManager.php b/includes/Core/Notifications/EmailManager.php index aef88be..14589fb 100644 --- a/includes/Core/Notifications/EmailManager.php +++ b/includes/Core/Notifications/EmailManager.php @@ -327,12 +327,19 @@ class EmailManager { return $message; // Use WordPress default } - // Build reset URL - use SPA route - $admin_url = admin_url('admin.php?page=woonoow'); + // Build reset URL - use customer-facing SPA route on my-account page + // The my-account page loads the customer-spa which has the reset-password route + $myaccount_page_id = function_exists('wc_get_page_id') ? wc_get_page_id('myaccount') : 0; + if ($myaccount_page_id > 0) { + $myaccount_url = get_permalink($myaccount_page_id); + } else { + // Fallback to home URL if my-account page doesn't exist + $myaccount_url = home_url('/'); + } // Build SPA reset password URL with hash router format - // Format: /wp-admin/admin.php?page=woonoow#/reset-password?key=KEY&login=LOGIN - $reset_link = $admin_url . '#/reset-password?key=' . $key . '&login=' . rawurlencode($user_login); + // Format: /my-account/#/reset-password?key=KEY&login=LOGIN + $reset_link = rtrim($myaccount_url, '/') . '#/reset-password?key=' . $key . '&login=' . rawurlencode($user_login); // Create a pseudo WC_Customer for template rendering $customer = null;