From 56042d4b8e5b6bb4b915237f2e5b5e057635fc67 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Wed, 31 Dec 2025 22:43:13 +0700 Subject: [PATCH] feat: add customer login page in SPA - Created Login/index.tsx with styled form - Added /auth/customer-login API endpoint (no admin perms required) - Registered route in Routes.php - Added /login route in customer-spa App.tsx - Account page now redirects to SPA login instead of wp-login.php - Login supports redirect param for post-login navigation --- customer-spa/src/App.tsx | 22 +-- customer-spa/src/pages/Account/index.tsx | 9 +- customer-spa/src/pages/Login/index.tsx | 165 +++++++++++++++++++++++ includes/Api/AuthController.php | 52 +++++++ includes/Api/Routes.php | 7 + 5 files changed, 242 insertions(+), 13 deletions(-) create mode 100644 customer-spa/src/pages/Login/index.tsx diff --git a/customer-spa/src/App.tsx b/customer-spa/src/App.tsx index b128883..e27240d 100644 --- a/customer-spa/src/App.tsx +++ b/customer-spa/src/App.tsx @@ -15,6 +15,7 @@ import Checkout from './pages/Checkout'; import ThankYou from './pages/ThankYou'; import Account from './pages/Account'; import Wishlist from './pages/Wishlist'; +import Login from './pages/Login'; // Create QueryClient instance const queryClient = new QueryClient({ @@ -30,7 +31,7 @@ const queryClient = new QueryClient({ // Get theme config from window (injected by PHP) const getThemeConfig = () => { const config = (window as any).woonoowCustomer?.theme; - + // Default config if not provided return config || { mode: 'full', @@ -65,28 +66,31 @@ const getInitialRoute = () => { function AppRoutes() { const initialRoute = getInitialRoute(); console.log('[WooNooW Customer] Using initial route:', initialRoute); - + return ( {/* Root route redirects to initial route based on SPA mode */} } /> - + {/* Shop Routes */} } /> } /> - + {/* Cart & Checkout */} } /> } /> } /> - + {/* Wishlist - Public route accessible to guests */} } /> - + + {/* Login */} + } /> + {/* My Account */} } /> - + {/* Fallback to initial route */} } /> @@ -98,14 +102,14 @@ function App() { const themeConfig = getThemeConfig(); const appearanceSettings = getAppearanceSettings(); const toastPosition = (appearanceSettings?.general?.toast_position || 'top-right') as any; - + return ( - + {/* Toast notifications - position from settings */} diff --git a/customer-spa/src/pages/Account/index.tsx b/customer-spa/src/pages/Account/index.tsx index a32b20e..1a19e98 100644 --- a/customer-spa/src/pages/Account/index.tsx +++ b/customer-spa/src/pages/Account/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Routes, Route, Navigate } from 'react-router-dom'; +import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; import Container from '@/components/Layout/Container'; import { AccountLayout } from './components/AccountLayout'; import Dashboard from './Dashboard'; @@ -12,11 +12,12 @@ import AccountDetails from './AccountDetails'; export default function Account() { const user = (window as any).woonoowCustomer?.user; - + const location = useLocation(); + // Redirect to login if not authenticated if (!user?.isLoggedIn) { - window.location.href = '/wp-login.php?redirect_to=' + encodeURIComponent(window.location.href); - return null; + const currentPath = location.pathname; + return ; } return ( diff --git a/customer-spa/src/pages/Login/index.tsx b/customer-spa/src/pages/Login/index.tsx new file mode 100644 index 0000000..f934fd1 --- /dev/null +++ b/customer-spa/src/pages/Login/index.tsx @@ -0,0 +1,165 @@ +import React, { useState } from 'react'; +import { useNavigate, useSearchParams, Link } 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 { LogIn, Eye, EyeOff, ArrowLeft } from 'lucide-react'; + +export default function Login() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const redirectTo = searchParams.get('redirect') || '/my-account'; + + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + 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/customer-login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': nonce, + }, + credentials: 'include', + body: JSON.stringify({ username, password }), + }); + + const data = await response.json(); + + if (data.success) { + // Update window config with new nonce and user data + if ((window as any).woonoowCustomer) { + (window as any).woonoowCustomer.nonce = data.nonce; + (window as any).woonoowCustomer.user = { + isLoggedIn: true, + id: data.user.id, + name: data.user.name, + email: data.user.email, + firstName: data.user.first_name, + lastName: data.user.last_name, + avatar: data.user.avatar, + }; + } + + toast.success('Login successful!'); + + // Full page reload to refresh nonce and user state + window.location.href = window.location.origin + '/store/#' + redirectTo; + } else { + setError(data.message || 'Login failed'); + } + } catch (err: any) { + setError(err.message || 'An error occurred. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + return ( + +
+
+ {/* Back link */} + + + Continue shopping + + +
+ {/* Header */} +
+
+ +
+

Welcome Back

+

Sign in to your account

+
+ + {/* Error message */} + {error && ( +
+ {error} +
+ )} + + {/* Form */} +
+
+ + setUsername(e.target.value)} + placeholder="Enter your email or username" + required + autoComplete="username" + disabled={isLoading} + /> +
+ +
+ +
+ setPassword(e.target.value)} + placeholder="Enter your password" + required + autoComplete="current-password" + disabled={isLoading} + className="pr-10" + /> + +
+
+ + +
+ + {/* Footer links */} + +
+
+
+
+ ); +} diff --git a/includes/Api/AuthController.php b/includes/Api/AuthController.php index f7918e9..849cfc9 100644 --- a/includes/Api/AuthController.php +++ b/includes/Api/AuthController.php @@ -78,6 +78,58 @@ class AuthController { ], 200 ); } + /** + * Customer login endpoint (no admin permission required) + * + * @param WP_REST_Request $request Request object + * @return WP_REST_Response Response object + */ + public static function customer_login( WP_REST_Request $request ): WP_REST_Response { + $username = sanitize_text_field( $request->get_param( 'username' ) ); + $password = $request->get_param( 'password' ); + + if ( empty( $username ) || empty( $password ) ) { + return new WP_REST_Response( [ + 'success' => false, + 'message' => __( 'Username and password are required', 'woonoow' ), + ], 400 ); + } + + // Authenticate user + $user = wp_authenticate( $username, $password ); + + if ( is_wp_error( $user ) ) { + return new WP_REST_Response( [ + 'success' => false, + 'message' => __( 'Invalid username or password', 'woonoow' ), + ], 401 ); + } + + // Clear old cookies and set new ones + wp_clear_auth_cookie(); + wp_set_current_user( $user->ID ); + wp_set_auth_cookie( $user->ID, true ); + + // Trigger login action + do_action( 'wp_login', $user->user_login, $user ); + + // Get customer data + $customer_data = [ + 'id' => $user->ID, + 'name' => $user->display_name, + 'email' => $user->user_email, + 'first_name' => get_user_meta( $user->ID, 'first_name', true ), + 'last_name' => get_user_meta( $user->ID, 'last_name', true ), + 'avatar' => get_avatar_url( $user->ID ), + ]; + + return new WP_REST_Response( [ + 'success' => true, + 'user' => $customer_data, + 'nonce' => wp_create_nonce( 'wp_rest' ), + ], 200 ); + } + /** * Logout endpoint * diff --git a/includes/Api/Routes.php b/includes/Api/Routes.php index e15cb2a..05d0105 100644 --- a/includes/Api/Routes.php +++ b/includes/Api/Routes.php @@ -65,6 +65,13 @@ class Routes { 'permission_callback' => '__return_true', ] ); + // Customer login endpoint (no admin permission required) + register_rest_route( $namespace, '/auth/customer-login', [ + 'methods' => 'POST', + 'callback' => [ AuthController::class, 'customer_login' ], + 'permission_callback' => '__return_true', + ] ); + // Defer to controllers to register their endpoints CheckoutController::register(); OrdersController::register();