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.
This commit is contained in:
parent
ab429f91dc
commit
90fa9443d4
140
apps/portal/src/features/auth/components/AuthModal/AuthModal.tsx
Normal file
140
apps/portal/src/features/auth/components/AuthModal/AuthModal.tsx
Normal file
@ -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 (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
onClick={e => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-background/80 backdrop-blur-sm transition-opacity"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div
|
||||
className="relative z-10 w-full max-w-lg rounded-2xl border border-border bg-card text-card-foreground shadow-[var(--cp-shadow-3)] max-h-[90vh] overflow-y-auto"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-border bg-card px-6 py-4">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-foreground">{title || defaultTitle}</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{description || defaultDescription}
|
||||
</p>
|
||||
</div>
|
||||
{showCloseButton && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="ml-4 text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label="Close modal"
|
||||
type="button"
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{mode === "signup" ? (
|
||||
<SignupForm
|
||||
redirectTo={redirectTo}
|
||||
onSuccess={() => {
|
||||
// Will be handled by useEffect above
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LoginForm
|
||||
redirectTo={redirectTo}
|
||||
onSuccess={() => {
|
||||
// Will be handled by useEffect above
|
||||
}}
|
||||
showSignupLink={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Toggle between signup and login */}
|
||||
<div className="mt-6 border-t border-border pt-6 text-center">
|
||||
{mode === "signup" ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Already have an account?{" "}
|
||||
<button
|
||||
onClick={() => setMode("login")}
|
||||
className="font-medium text-primary hover:underline transition-colors"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Don't have an account?{" "}
|
||||
<button
|
||||
onClick={() => setMode("signup")}
|
||||
className="font-medium text-primary hover:underline transition-colors"
|
||||
>
|
||||
Create one
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export { AuthModal } from "./AuthModal";
|
||||
@ -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(
|
||||
|
||||
@ -1,49 +1,194 @@
|
||||
"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 (
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
||||
<CatalogBackLink href={`${shopBasePath}/internet`} label="Back to Internet plans" />
|
||||
|
||||
<CatalogHero
|
||||
title="Step 1: Create your account"
|
||||
description="We’ll verify service availability for your address, then show personalized internet plans and configuration options."
|
||||
/>
|
||||
|
||||
<AlertBanner variant="info" title="Already have an account?" className="max-w-2xl mx-auto">
|
||||
<div className="space-y-3 text-sm text-foreground/80">
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<Button
|
||||
as="a"
|
||||
href={`/auth/login?redirect=${encodeURIComponent("/account/shop/internet")}`}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AlertBanner>
|
||||
|
||||
<div className="mt-8 bg-card border border-border rounded-2xl p-6 md:p-7 shadow-[var(--cp-shadow-1)]">
|
||||
<SignupForm redirectTo="/account/shop/internet" />
|
||||
<div className="mt-8 space-y-6">
|
||||
<Skeleton className="h-10 w-96" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!plan) {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
||||
<CatalogBackLink href={`${shopBasePath}/internet`} label="Back to Internet plans" />
|
||||
<AlertBanner variant="error" title="Plan not found">
|
||||
The selected plan could not be found. Please go back and select a plan.
|
||||
</AlertBanner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
||||
<CatalogBackLink href={`${shopBasePath}/internet`} label="Back to Internet plans" />
|
||||
|
||||
<CatalogHero
|
||||
title="Get started with your internet plan"
|
||||
description="Create an account to verify service availability for your address and configure your plan."
|
||||
/>
|
||||
|
||||
{/* Plan Summary Card */}
|
||||
<div className="mt-8 bg-card border border-border rounded-2xl p-6 md:p-8 shadow-[var(--cp-shadow-1)]">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 border border-primary/20">
|
||||
<ServerIcon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xl font-semibold text-foreground mb-2">{plan.name}</h3>
|
||||
{plan.description && (
|
||||
<p className="text-sm text-muted-foreground mb-4">{plan.description}</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{plan.internetPlanTier && (
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary/10 text-primary border border-primary/20">
|
||||
{plan.internetPlanTier} Tier
|
||||
</span>
|
||||
)}
|
||||
{plan.internetOfferingType && (
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-muted text-muted-foreground border border-border">
|
||||
{plan.internetOfferingType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<CardPricing
|
||||
monthlyPrice={plan.monthlyPrice}
|
||||
oneTimePrice={plan.oneTimePrice}
|
||||
size="lg"
|
||||
alignment="left"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan Features */}
|
||||
{plan.catalogMetadata?.features && plan.catalogMetadata.features.length > 0 && (
|
||||
<div className="border-t border-border pt-6 mt-6">
|
||||
<h4 className="text-sm font-semibold text-foreground mb-4 uppercase tracking-wide">
|
||||
Plan Includes:
|
||||
</h4>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{plan.catalogMetadata.features.slice(0, 6).map((feature, index) => {
|
||||
const [label, detail] = feature.split(":");
|
||||
return (
|
||||
<li key={index} className="flex items-start gap-2">
|
||||
<CheckIcon className="h-4 w-4 text-success mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{label.trim()}
|
||||
{detail && (
|
||||
<>
|
||||
: <span className="text-foreground font-medium">{detail.trim()}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Auth Prompt */}
|
||||
<div className="mt-8 bg-muted/50 border border-border rounded-2xl p-6 md:p-8">
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">Ready to get started?</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-2xl mx-auto">
|
||||
Create an account to verify service availability for your address and complete your
|
||||
order. We'll guide you through the setup process.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center max-w-md mx-auto">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setAuthMode("signup");
|
||||
setAuthModalOpen(true);
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setAuthMode("login");
|
||||
setAuthModalOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-border">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground mb-1">Verify Availability</div>
|
||||
<div className="text-xs text-muted-foreground">Check service at your address</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground mb-1">Personalized Plans</div>
|
||||
<div className="text-xs text-muted-foreground">See plans tailored to you</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground mb-1">Secure Ordering</div>
|
||||
<div className="text-xs text-muted-foreground">Complete your order safely</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AuthModal
|
||||
isOpen={authModalOpen}
|
||||
onClose={() => 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."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicInternetConfigureView;
|
||||
|
||||
@ -129,7 +129,7 @@ export function PublicInternetPlansView() {
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
||||
<Button as="a" href="/auth/signup" className="whitespace-nowrap">
|
||||
Create account to check availability
|
||||
Get started
|
||||
</Button>
|
||||
<Button as="a" href="/auth/login" variant="outline" className="whitespace-nowrap">
|
||||
Sign in
|
||||
@ -149,8 +149,8 @@ export function PublicInternetPlansView() {
|
||||
disabled={false}
|
||||
pricingPrefix="Starting from"
|
||||
action={{
|
||||
label: "Create account",
|
||||
href: `/auth/signup?redirect=${encodeURIComponent("/account/shop/internet")}`,
|
||||
label: "Get started",
|
||||
href: `/shop/internet/configure?planSku=${encodeURIComponent(plan.sku)}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,53 +1,208 @@
|
||||
"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 (
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
||||
<CatalogBackLink href={`${shopBasePath}/sim`} label="Back to SIM plans" />
|
||||
|
||||
<CatalogHero
|
||||
title="Step 1: Create your account"
|
||||
description="Ordering requires a payment method and identity verification."
|
||||
/>
|
||||
|
||||
<AlertBanner variant="info" title="Already have an account?" className="max-w-2xl mx-auto">
|
||||
<div className="space-y-3 text-sm text-foreground/80">
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<Button
|
||||
as="a"
|
||||
href={`/auth/login?redirect=${encodeURIComponent(redirectTarget)}`}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AlertBanner>
|
||||
|
||||
<div className="mt-8 bg-card border border-border rounded-2xl p-6 md:p-7 shadow-[var(--cp-shadow-1)]">
|
||||
<SignupForm redirectTo={redirectTarget} />
|
||||
<div className="mt-8 space-y-6">
|
||||
<Skeleton className="h-10 w-96" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!plan) {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
||||
<CatalogBackLink href={`${shopBasePath}/sim`} label="Back to SIM plans" />
|
||||
<AlertBanner variant="error" title="Plan not found">
|
||||
The selected plan could not be found. Please go back and select a plan.
|
||||
</AlertBanner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
||||
<CatalogBackLink href={`${shopBasePath}/sim`} label="Back to SIM plans" />
|
||||
|
||||
<CatalogHero
|
||||
title="Get started with your SIM plan"
|
||||
description="Create an account to complete your order, add a payment method, and complete identity verification."
|
||||
/>
|
||||
|
||||
{/* Plan Summary Card */}
|
||||
<div className="mt-8 bg-card border border-border rounded-2xl p-6 md:p-8 shadow-[var(--cp-shadow-1)]">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 border border-primary/20">
|
||||
<DevicePhoneMobileIcon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xl font-semibold text-foreground mb-2">{plan.name}</h3>
|
||||
{plan.description && (
|
||||
<p className="text-sm text-muted-foreground mb-4">{plan.description}</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{plan.simPlanType && (
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary/10 text-primary border border-primary/20">
|
||||
{plan.simPlanType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<CardPricing
|
||||
monthlyPrice={plan.monthlyPrice}
|
||||
oneTimePrice={plan.oneTimePrice}
|
||||
size="lg"
|
||||
alignment="left"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan Details */}
|
||||
{(plan.simDataSize || plan.description) && (
|
||||
<div className="border-t border-border pt-6 mt-6">
|
||||
<h4 className="text-sm font-semibold text-foreground mb-4 uppercase tracking-wide">
|
||||
Plan Details:
|
||||
</h4>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{plan.simDataSize && (
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckIcon className="h-4 w-4 text-success mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Data: <span className="text-foreground font-medium">{plan.simDataSize}</span>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{plan.simPlanType && (
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckIcon className="h-4 w-4 text-success mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Type: <span className="text-foreground font-medium">{plan.simPlanType}</span>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{plan.simHasFamilyDiscount && (
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckIcon className="h-4 w-4 text-success mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-foreground font-medium">Family Discount Available</span>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{plan.billingCycle && (
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckIcon className="h-4 w-4 text-success mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Billing:{" "}
|
||||
<span className="text-foreground font-medium">{plan.billingCycle}</span>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Auth Prompt */}
|
||||
<div className="mt-8 bg-muted/50 border border-border rounded-2xl p-6 md:p-8">
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">Ready to order?</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-2xl mx-auto">
|
||||
Create an account to complete your SIM order. You'll need to add a payment method
|
||||
and complete identity verification.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center max-w-md mx-auto">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setAuthMode("signup");
|
||||
setAuthModalOpen(true);
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setAuthMode("login");
|
||||
setAuthModalOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-border">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground mb-1">Secure Payment</div>
|
||||
<div className="text-xs text-muted-foreground">Add payment method safely</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground mb-1">
|
||||
Identity Verification
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Complete verification process</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground mb-1">Order Management</div>
|
||||
<div className="text-xs text-muted-foreground">Track your order status</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AuthModal
|
||||
isOpen={authModalOpen}
|
||||
onClose={() => setAuthModalOpen(false)}
|
||||
initialMode={authMode}
|
||||
redirectTo={redirectTarget}
|
||||
title="Create your account"
|
||||
description="Ordering requires a payment method and identity verification."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicSimConfigureView;
|
||||
|
||||
@ -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.
|
||||
</p>
|
||||
<div className="flex gap-3 sm:ml-auto">
|
||||
<Button as="a" href={buildRedirect()} size="sm" className="whitespace-nowrap">
|
||||
Create account
|
||||
<Button as="a" href="/auth/signup" size="sm" className="whitespace-nowrap">
|
||||
Get started
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
|
||||
98
docs/shop-ux-improvements.md
Normal file
98
docs/shop-ux-improvements.md
Normal file
@ -0,0 +1,98 @@
|
||||
# Shop Experience UX Improvements
|
||||
|
||||
## Overview
|
||||
|
||||
The shop experience has been significantly improved to be more user-friendly and less disruptive. Instead of redirecting users to a full signup page when they want to configure a plan, we now show plan context and use a modal for authentication.
|
||||
|
||||
## Key Improvements
|
||||
|
||||
### 1. **AuthModal Component** ✨
|
||||
|
||||
- **Location**: `apps/portal/src/features/auth/components/AuthModal/`
|
||||
- **Purpose**: Reusable modal component for signup/login that doesn't break the shopping flow
|
||||
- **Features**:
|
||||
- Overlay design that keeps users in context
|
||||
- Toggle between signup and login modes
|
||||
- Automatic redirect after successful authentication
|
||||
- Customizable title and description
|
||||
- Responsive design
|
||||
|
||||
### 2. **Improved Configure Pages**
|
||||
|
||||
Both `PublicInternetConfigureView` and `PublicSimConfigureView` now:
|
||||
|
||||
- **Show plan information** before requiring authentication
|
||||
- Plan name, description, pricing
|
||||
- Plan features and badges
|
||||
- Visual plan summary card
|
||||
- **Use modal authentication** instead of full-page redirects
|
||||
- Users stay on the configure page
|
||||
- Can see what they're signing up for
|
||||
- Better context preservation
|
||||
- **Better loading and error states**
|
||||
- Skeleton loaders while fetching plan data
|
||||
- Clear error messages if plan not found
|
||||
|
||||
### 3. **Improved Plan Card CTAs**
|
||||
|
||||
- Changed button labels from "Create account" to "Get started" (more inviting)
|
||||
- Plan cards now link to configure pages instead of directly to signup
|
||||
- Configure pages show plan context before requiring auth
|
||||
|
||||
### 4. **Better User Flow**
|
||||
|
||||
**Before:**
|
||||
|
||||
1. User browses plans
|
||||
2. Clicks "Create account" → Redirected to full signup page
|
||||
3. Loses context of what plan they selected
|
||||
4. Must navigate back after signup
|
||||
|
||||
**After:**
|
||||
|
||||
1. User browses plans
|
||||
2. Clicks "Get started" → Goes to configure page
|
||||
3. Sees plan details and context
|
||||
4. Clicks "Create account" → Modal opens
|
||||
5. Completes signup/login in modal
|
||||
6. Automatically redirected to authenticated configure page
|
||||
7. Never loses context
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Components Created
|
||||
|
||||
- `AuthModal` - Reusable authentication modal
|
||||
- Updated `PublicInternetConfigureView` - Shows plan context with modal auth
|
||||
- Updated `PublicSimConfigureView` - Shows plan context with modal auth
|
||||
|
||||
### Components Updated
|
||||
|
||||
- `LoginForm` - Added `redirectTo` prop for modal usage
|
||||
- `PublicInternetPlans` - Improved CTAs and redirects
|
||||
- `PublicSimPlans` - Improved CTAs and redirects
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Context Preservation**: Users always see what plan they're configuring
|
||||
- **Progressive Disclosure**: Plan details shown before requiring authentication
|
||||
- **Non-Blocking**: Modal can be closed to continue browsing
|
||||
- **Seamless Flow**: Automatic redirect after authentication maintains user intent
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Better UX**: Users don't lose context when authenticating
|
||||
2. **Higher Conversion**: Less friction in the signup process
|
||||
3. **Clearer Intent**: Users see what they're signing up for
|
||||
4. **Professional Feel**: Modal-based auth feels more modern
|
||||
5. **Flexible**: Easy to reuse AuthModal in other parts of the app
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for the future:
|
||||
|
||||
- Add "Continue as guest" option (if business logic allows)
|
||||
- Show more plan details before auth (pricing breakdown, installation options)
|
||||
- Add plan comparison before auth
|
||||
- Remember selected plan in localStorage for returning visitors
|
||||
- Add social login options in the modal
|
||||
Loading…
x
Reference in New Issue
Block a user