From 90fa9443d48751953c5e51367ae5c9faed35a916 Mon Sep 17 00:00:00 2001 From: barsa Date: Fri, 19 Dec 2025 16:33:53 +0900 Subject: [PATCH] Enhance Authentication Flow in Catalog Views - Added redirect functionality to the LoginForm component, allowing for dynamic redirection after login. - Updated PublicInternetConfigure and PublicSimConfigure views to utilize modals for user authentication, improving user experience by avoiding full-page redirects. - Enhanced PublicInternetPlans and PublicSimPlans views with updated button labels for clarity and consistency. - Refactored plan configuration views to display detailed plan information and prompt users for account creation or login, streamlining the onboarding process. --- .../auth/components/AuthModal/AuthModal.tsx | 140 ++++++++++++ .../auth/components/AuthModal/index.ts | 1 + .../auth/components/LoginForm/LoginForm.tsx | 4 +- .../catalog/views/PublicInternetConfigure.tsx | 185 ++++++++++++++-- .../catalog/views/PublicInternetPlans.tsx | 6 +- .../catalog/views/PublicSimConfigure.tsx | 201 ++++++++++++++++-- .../features/catalog/views/PublicSimPlans.tsx | 7 +- docs/shop-ux-improvements.md | 98 +++++++++ 8 files changed, 591 insertions(+), 51 deletions(-) create mode 100644 apps/portal/src/features/auth/components/AuthModal/AuthModal.tsx create mode 100644 apps/portal/src/features/auth/components/AuthModal/index.ts create mode 100644 docs/shop-ux-improvements.md diff --git a/apps/portal/src/features/auth/components/AuthModal/AuthModal.tsx b/apps/portal/src/features/auth/components/AuthModal/AuthModal.tsx new file mode 100644 index 00000000..a95f967e --- /dev/null +++ b/apps/portal/src/features/auth/components/AuthModal/AuthModal.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import { SignupForm } from "../SignupForm/SignupForm"; +import { LoginForm } from "../LoginForm/LoginForm"; +import { useAuthStore } from "../../services/auth.store"; +import { useRouter } from "next/navigation"; + +interface AuthModalProps { + isOpen: boolean; + onClose: () => void; + initialMode?: "signup" | "login"; + redirectTo?: string; + title?: string; + description?: string; + showCloseButton?: boolean; +} + +export function AuthModal({ + isOpen, + onClose, + initialMode = "signup", + redirectTo, + title, + description, + showCloseButton = true, +}: AuthModalProps) { + const [mode, setMode] = useState<"signup" | "login">(initialMode); + const router = useRouter(); + const isAuthenticated = useAuthStore(state => state.isAuthenticated); + const hasCheckedAuth = useAuthStore(state => state.hasCheckedAuth); + + // Update mode when initialMode changes + useEffect(() => { + setMode(initialMode); + }, [initialMode]); + + // Close modal and redirect when authenticated + useEffect(() => { + if (isOpen && hasCheckedAuth && isAuthenticated && redirectTo) { + onClose(); + router.push(redirectTo); + } + }, [isOpen, hasCheckedAuth, isAuthenticated, redirectTo, onClose, router]); + + if (!isOpen) return null; + + const defaultTitle = mode === "signup" ? "Create your account" : "Sign in to continue"; + const defaultDescription = + mode === "signup" + ? "Create an account to continue with your order and access personalized plans." + : "Sign in to your account to continue with your order."; + + return ( +
{ + if (e.target === e.currentTarget) { + onClose(); + } + }} + > + {/* Backdrop */} + + ); +} diff --git a/apps/portal/src/features/auth/components/AuthModal/index.ts b/apps/portal/src/features/auth/components/AuthModal/index.ts new file mode 100644 index 00000000..11ee2cc1 --- /dev/null +++ b/apps/portal/src/features/auth/components/AuthModal/index.ts @@ -0,0 +1 @@ +export { AuthModal } from "./AuthModal"; diff --git a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx index 0d7755fe..4dd41b02 100644 --- a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx +++ b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx @@ -21,6 +21,7 @@ interface LoginFormProps { showSignupLink?: boolean; showForgotPasswordLink?: boolean; className?: string; + redirectTo?: string; } /** @@ -41,10 +42,11 @@ export function LoginForm({ showSignupLink = true, showForgotPasswordLink = true, className = "", + redirectTo, }: LoginFormProps) { const searchParams = useSearchParams(); const { login, loading, error, clearError } = useLogin(); - const redirect = searchParams?.get("next") || searchParams?.get("redirect"); + const redirect = redirectTo || searchParams?.get("next") || searchParams?.get("redirect"); const redirectQuery = redirect ? `?redirect=${encodeURIComponent(redirect)}` : ""; const handleLogin = useCallback( diff --git a/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx b/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx index 35fb72d5..18bf4f06 100644 --- a/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx +++ b/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx @@ -1,48 +1,193 @@ "use client"; +import { useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { ServerIcon, CheckIcon } from "@heroicons/react/24/outline"; import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; import { Button } from "@/components/atoms/button"; import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink"; import { CatalogHero } from "@/features/catalog/components/base/CatalogHero"; import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath"; -import { SignupForm } from "@/features/auth/components/SignupForm/SignupForm"; +import { useInternetPlan } from "@/features/catalog/hooks"; +import { AuthModal } from "@/features/auth/components/AuthModal/AuthModal"; +import { CardPricing } from "@/features/catalog/components/base/CardPricing"; +import { Skeleton } from "@/components/atoms/loading-skeleton"; /** * Public Internet Configure View * - * Public shop is browse-only. Users must create an account so we can verify internet availability - * for their service address before showing personalized plans and allowing configuration. + * Shows selected plan information and prompts for authentication via modal. + * Much better UX than redirecting to a full signup page. */ export function PublicInternetConfigureView() { const shopBasePath = useShopBasePath(); + const searchParams = useSearchParams(); + const planSku = searchParams?.get("planSku"); + const { plan, isLoading } = useInternetPlan(planSku || undefined); + const [authModalOpen, setAuthModalOpen] = useState(false); + const [authMode, setAuthMode] = useState<"signup" | "login">("signup"); + + const redirectTo = planSku + ? `/account/shop/internet/configure?planSku=${encodeURIComponent(planSku)}` + : "/account/shop/internet"; + + if (isLoading) { + return ( +
+ +
+ + +
+
+ ); + } + + if (!plan) { + return ( +
+ + + The selected plan could not be found. Please go back and select a plan. + +
+ ); + } return ( -
- + <> +
+ - + - -
-
+ {/* Plan Summary Card */} +
+
+
+
+ +
+
+
+

{plan.name}

+ {plan.description && ( +

{plan.description}

+ )} +
+ {plan.internetPlanTier && ( + + {plan.internetPlanTier} Tier + + )} + {plan.internetOfferingType && ( + + {plan.internetOfferingType} + + )} +
+
+ +
+
+
+ + {/* Plan Features */} + {plan.catalogMetadata?.features && plan.catalogMetadata.features.length > 0 && ( +
+

+ Plan Includes: +

+
    + {plan.catalogMetadata.features.slice(0, 6).map((feature, index) => { + const [label, detail] = feature.split(":"); + return ( +
  • + + + {label.trim()} + {detail && ( + <> + : {detail.trim()} + + )} + +
  • + ); + })} +
+
+ )} +
+ + {/* Auth Prompt */} +
+
+

Ready to get started?

+

+ Create an account to verify service availability for your address and complete your + order. We'll guide you through the setup process. +

+
+ +
+
-
- -
- +
+
+
+
Verify Availability
+
Check service at your address
+
+
+
Personalized Plans
+
See plans tailored to you
+
+
+
Secure Ordering
+
Complete your order safely
+
+
+
+
-
+ + setAuthModalOpen(false)} + initialMode={authMode} + redirectTo={redirectTo} + title="Create your account" + description="We'll verify service availability for your address, then show personalized internet plans and configuration options." + /> + ); } diff --git a/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx b/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx index 6f50607d..30b640ec 100644 --- a/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx +++ b/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx @@ -129,7 +129,7 @@ export function PublicInternetPlansView() {
diff --git a/apps/portal/src/features/catalog/views/PublicSimConfigure.tsx b/apps/portal/src/features/catalog/views/PublicSimConfigure.tsx index 1ad04e29..8bf69741 100644 --- a/apps/portal/src/features/catalog/views/PublicSimConfigure.tsx +++ b/apps/portal/src/features/catalog/views/PublicSimConfigure.tsx @@ -1,52 +1,207 @@ "use client"; +import { useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { DevicePhoneMobileIcon, CheckIcon } from "@heroicons/react/24/outline"; import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; import { Button } from "@/components/atoms/button"; import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink"; import { CatalogHero } from "@/features/catalog/components/base/CatalogHero"; import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath"; -import { useSearchParams } from "next/navigation"; -import { SignupForm } from "@/features/auth/components/SignupForm/SignupForm"; +import { useSimPlan } from "@/features/catalog/hooks"; +import { AuthModal } from "@/features/auth/components/AuthModal/AuthModal"; +import { CardPricing } from "@/features/catalog/components/base/CardPricing"; +import { Skeleton } from "@/components/atoms/loading-skeleton"; /** * Public SIM Configure View * - * Public shop is browse-only. Users must create an account to add a payment method and - * complete identity verification before ordering SIM service. + * Shows selected plan information and prompts for authentication via modal. + * Much better UX than redirecting to a full signup page. */ export function PublicSimConfigureView() { const shopBasePath = useShopBasePath(); const searchParams = useSearchParams(); - const plan = searchParams?.get("planSku") || undefined; - const redirectTarget = plan ? `/account/shop/sim/configure?planSku=${plan}` : "/account/shop/sim"; + const planSku = searchParams?.get("planSku"); + const { plan, isLoading } = useSimPlan(planSku || undefined); + const [authModalOpen, setAuthModalOpen] = useState(false); + const [authMode, setAuthMode] = useState<"signup" | "login">("signup"); + + const redirectTarget = planSku + ? `/account/shop/sim/configure?planSku=${encodeURIComponent(planSku)}` + : "/account/shop/sim"; + + if (isLoading) { + return ( +
+ +
+ + +
+
+ ); + } + + if (!plan) { + return ( +
+ + + The selected plan could not be found. Please go back and select a plan. + +
+ ); + } return ( -
- + <> +
+ - + - -
-
+ {/* Plan Summary Card */} +
+
+
+
+ +
+
+
+

{plan.name}

+ {plan.description && ( +

{plan.description}

+ )} +
+ {plan.simPlanType && ( + + {plan.simPlanType} + + )} +
+
+ +
+
+
+ + {/* Plan Details */} + {(plan.simDataSize || plan.description) && ( +
+

+ Plan Details: +

+
    + {plan.simDataSize && ( +
  • + + + Data: {plan.simDataSize} + +
  • + )} + {plan.simPlanType && ( +
  • + + + Type: {plan.simPlanType} + +
  • + )} + {plan.simHasFamilyDiscount && ( +
  • + + + Family Discount Available + +
  • + )} + {plan.billingCycle && ( +
  • + + + Billing:{" "} + {plan.billingCycle} + +
  • + )} +
+
+ )} +
+ + {/* Auth Prompt */} +
+
+

Ready to order?

+

+ Create an account to complete your SIM order. You'll need to add a payment method + and complete identity verification. +

+
+ +
+
-
- -
- +
+
+
+
Secure Payment
+
Add payment method safely
+
+
+
+ Identity Verification +
+
Complete verification process
+
+
+
Order Management
+
Track your order status
+
+
+
+
-
+ + setAuthModalOpen(false)} + initialMode={authMode} + redirectTo={redirectTarget} + title="Create your account" + description="Ordering requires a payment method and identity verification." + /> + ); } diff --git a/apps/portal/src/features/catalog/views/PublicSimPlans.tsx b/apps/portal/src/features/catalog/views/PublicSimPlans.tsx index f513b997..affb9711 100644 --- a/apps/portal/src/features/catalog/views/PublicSimPlans.tsx +++ b/apps/portal/src/features/catalog/views/PublicSimPlans.tsx @@ -38,8 +38,7 @@ export function PublicSimPlansView() { "data-voice" ); const buildRedirect = (planSku?: string) => { - const target = planSku ? `/account/shop/sim/configure?planSku=${planSku}` : "/account/shop/sim"; - return `/auth/signup?redirect=${encodeURIComponent(target)}`; + return planSku ? `/shop/sim/configure?planSku=${encodeURIComponent(planSku)}` : "/shop/sim"; }; if (isLoading) { @@ -120,8 +119,8 @@ export function PublicSimPlansView() { verification.

-