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 */}
+
+
+ {/* Modal */}
+
e.stopPropagation()}
+ >
+
+
+
{title || defaultTitle}
+
+ {description || defaultDescription}
+
+
+ {showCloseButton && (
+
+ )}
+
+
+
+ {mode === "signup" ? (
+
{
+ // Will be handled by useEffect above
+ }}
+ />
+ ) : (
+ {
+ // Will be handled by useEffect above
+ }}
+ showSignupLink={false}
+ />
+ )}
+
+ {/* Toggle between signup and login */}
+
+ {mode === "signup" ? (
+
+ Already have an account?{" "}
+
+
+ ) : (
+
+ Don't have an account?{" "}
+
+
+ )}
+
+
+
+
+ );
+}
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.
-