diff --git a/apps/portal/src/app/(authenticated)/subscriptions/[id]/page.tsx b/apps/portal/src/app/account/services/[id]/page.tsx
similarity index 72%
rename from apps/portal/src/app/(authenticated)/subscriptions/[id]/page.tsx
rename to apps/portal/src/app/account/services/[id]/page.tsx
index 4073bbde..2324febc 100644
--- a/apps/portal/src/app/(authenticated)/subscriptions/[id]/page.tsx
+++ b/apps/portal/src/app/account/services/[id]/page.tsx
@@ -1,5 +1,5 @@
import SubscriptionDetailContainer from "@/features/subscriptions/views/SubscriptionDetail";
-export default function SubscriptionDetailPage() {
+export default function AccountServiceDetailPage() {
return
;
}
diff --git a/apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/call-history/page.tsx b/apps/portal/src/app/account/services/[id]/sim/call-history/page.tsx
similarity index 70%
rename from apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/call-history/page.tsx
rename to apps/portal/src/app/account/services/[id]/sim/call-history/page.tsx
index efa22ee9..16e52603 100644
--- a/apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/call-history/page.tsx
+++ b/apps/portal/src/app/account/services/[id]/sim/call-history/page.tsx
@@ -1,6 +1,5 @@
import SimCallHistoryContainer from "@/features/subscriptions/views/SimCallHistory";
-export default function SimCallHistoryPage() {
+export default function AccountSimCallHistoryPage() {
return
;
}
-
diff --git a/apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/cancel/page.tsx b/apps/portal/src/app/account/services/[id]/sim/cancel/page.tsx
similarity index 69%
rename from apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/cancel/page.tsx
rename to apps/portal/src/app/account/services/[id]/sim/cancel/page.tsx
index c103af27..f9aaf9a4 100644
--- a/apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/cancel/page.tsx
+++ b/apps/portal/src/app/account/services/[id]/sim/cancel/page.tsx
@@ -1,5 +1,5 @@
import SimCancelContainer from "@/features/subscriptions/views/SimCancel";
-export default function SimCancelPage() {
+export default function AccountSimCancelPage() {
return
;
}
diff --git a/apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/change-plan/page.tsx b/apps/portal/src/app/account/services/[id]/sim/change-plan/page.tsx
similarity index 69%
rename from apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/change-plan/page.tsx
rename to apps/portal/src/app/account/services/[id]/sim/change-plan/page.tsx
index 4ad4d81d..8ff1da30 100644
--- a/apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/change-plan/page.tsx
+++ b/apps/portal/src/app/account/services/[id]/sim/change-plan/page.tsx
@@ -1,5 +1,5 @@
import SimChangePlanContainer from "@/features/subscriptions/views/SimChangePlan";
-export default function SimChangePlanPage() {
+export default function AccountSimChangePlanPage() {
return
;
}
diff --git a/apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/reissue/page.tsx b/apps/portal/src/app/account/services/[id]/sim/reissue/page.tsx
similarity index 69%
rename from apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/reissue/page.tsx
rename to apps/portal/src/app/account/services/[id]/sim/reissue/page.tsx
index e99470f2..1936a048 100644
--- a/apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/reissue/page.tsx
+++ b/apps/portal/src/app/account/services/[id]/sim/reissue/page.tsx
@@ -1,5 +1,5 @@
import SimReissueContainer from "@/features/subscriptions/views/SimReissue";
-export default function SimReissuePage() {
+export default function AccountSimReissuePage() {
return
;
}
diff --git a/apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/top-up/page.tsx b/apps/portal/src/app/account/services/[id]/sim/top-up/page.tsx
similarity index 69%
rename from apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/top-up/page.tsx
rename to apps/portal/src/app/account/services/[id]/sim/top-up/page.tsx
index 0c7da26a..89629c2e 100644
--- a/apps/portal/src/app/(authenticated)/subscriptions/[id]/sim/top-up/page.tsx
+++ b/apps/portal/src/app/account/services/[id]/sim/top-up/page.tsx
@@ -1,5 +1,5 @@
import SimTopUpContainer from "@/features/subscriptions/views/SimTopUp";
-export default function SimTopUpPage() {
+export default function AccountSimTopUpPage() {
return
;
}
diff --git a/apps/portal/src/app/(authenticated)/subscriptions/loading.tsx b/apps/portal/src/app/account/services/loading.tsx
similarity index 72%
rename from apps/portal/src/app/(authenticated)/subscriptions/loading.tsx
rename to apps/portal/src/app/account/services/loading.tsx
index 82717262..3db64007 100644
--- a/apps/portal/src/app/(authenticated)/subscriptions/loading.tsx
+++ b/apps/portal/src/app/account/services/loading.tsx
@@ -2,12 +2,12 @@ import { RouteLoading } from "@/components/molecules/RouteLoading";
import { ServerIcon } from "@heroicons/react/24/outline";
import { LoadingTable } from "@/components/atoms/loading-skeleton";
-export default function SubscriptionsLoading() {
+export default function AccountServicesLoading() {
return (
}
- title="Subscriptions"
- description="View and manage your subscriptions"
+ title="Services"
+ description="View and manage your services"
mode="content"
>
diff --git a/apps/portal/src/app/(authenticated)/subscriptions/page.tsx b/apps/portal/src/app/account/services/page.tsx
similarity index 73%
rename from apps/portal/src/app/(authenticated)/subscriptions/page.tsx
rename to apps/portal/src/app/account/services/page.tsx
index c6bef54d..13e6e5bb 100644
--- a/apps/portal/src/app/(authenticated)/subscriptions/page.tsx
+++ b/apps/portal/src/app/account/services/page.tsx
@@ -1,5 +1,5 @@
import SubscriptionsListContainer from "@/features/subscriptions/views/SubscriptionsList";
-export default function SubscriptionsPage() {
+export default function AccountServicesPage() {
return
;
}
diff --git a/apps/portal/src/app/(authenticated)/account/loading.tsx b/apps/portal/src/app/account/settings/loading.tsx
similarity index 94%
rename from apps/portal/src/app/(authenticated)/account/loading.tsx
rename to apps/portal/src/app/account/settings/loading.tsx
index 2e925894..77d7c570 100644
--- a/apps/portal/src/app/(authenticated)/account/loading.tsx
+++ b/apps/portal/src/app/account/settings/loading.tsx
@@ -2,11 +2,11 @@ import { RouteLoading } from "@/components/molecules/RouteLoading";
import { UserIcon } from "@heroicons/react/24/outline";
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
-export default function AccountLoading() {
+export default function AccountSettingsLoading() {
return (
}
- title="Account"
+ title="Settings"
description="Loading your profile..."
mode="content"
>
diff --git a/apps/portal/src/app/(authenticated)/account/profile/page.tsx b/apps/portal/src/app/account/settings/page.tsx
similarity index 69%
rename from apps/portal/src/app/(authenticated)/account/profile/page.tsx
rename to apps/portal/src/app/account/settings/page.tsx
index b97286f7..ff9565c5 100644
--- a/apps/portal/src/app/(authenticated)/account/profile/page.tsx
+++ b/apps/portal/src/app/account/settings/page.tsx
@@ -1,5 +1,5 @@
import ProfileContainer from "@/features/account/views/ProfileContainer";
-export default function ProfilePage() {
+export default function AccountSettingsPage() {
return
;
}
diff --git a/apps/portal/src/app/(authenticated)/support/cases/[id]/page.tsx b/apps/portal/src/app/account/support/[id]/page.tsx
similarity index 70%
rename from apps/portal/src/app/(authenticated)/support/cases/[id]/page.tsx
rename to apps/portal/src/app/account/support/[id]/page.tsx
index 0bcc3feb..bece2764 100644
--- a/apps/portal/src/app/(authenticated)/support/cases/[id]/page.tsx
+++ b/apps/portal/src/app/account/support/[id]/page.tsx
@@ -4,8 +4,7 @@ interface PageProps {
params: Promise<{ id: string }>;
}
-export default async function SupportCaseDetailPage({ params }: PageProps) {
+export default async function AccountSupportCaseDetailPage({ params }: PageProps) {
const { id } = await params;
return
;
}
-
diff --git a/apps/portal/src/app/(authenticated)/support/cases/loading.tsx b/apps/portal/src/app/account/support/loading.tsx
similarity index 90%
rename from apps/portal/src/app/(authenticated)/support/cases/loading.tsx
rename to apps/portal/src/app/account/support/loading.tsx
index 3c933597..833b5692 100644
--- a/apps/portal/src/app/(authenticated)/support/cases/loading.tsx
+++ b/apps/portal/src/app/account/support/loading.tsx
@@ -2,7 +2,7 @@ import { RouteLoading } from "@/components/molecules/RouteLoading";
import { ChatBubbleLeftRightIcon } from "@heroicons/react/24/outline";
import { LoadingTable } from "@/components/atoms/loading-skeleton";
-export default function SupportCasesLoading() {
+export default function AccountSupportLoading() {
return (
}
diff --git a/apps/portal/src/app/(authenticated)/support/new/loading.tsx b/apps/portal/src/app/account/support/new/loading.tsx
similarity index 94%
rename from apps/portal/src/app/(authenticated)/support/new/loading.tsx
rename to apps/portal/src/app/account/support/new/loading.tsx
index 6da32d5c..bad05834 100644
--- a/apps/portal/src/app/(authenticated)/support/new/loading.tsx
+++ b/apps/portal/src/app/account/support/new/loading.tsx
@@ -2,7 +2,7 @@ import { RouteLoading } from "@/components/molecules/RouteLoading";
import { PencilSquareIcon } from "@heroicons/react/24/outline";
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
-export default function NewSupportLoading() {
+export default function AccountSupportNewLoading() {
return (
}
diff --git a/apps/portal/src/app/(authenticated)/support/new/page.tsx b/apps/portal/src/app/account/support/new/page.tsx
similarity index 63%
rename from apps/portal/src/app/(authenticated)/support/new/page.tsx
rename to apps/portal/src/app/account/support/new/page.tsx
index 65c960da..730dceac 100644
--- a/apps/portal/src/app/(authenticated)/support/new/page.tsx
+++ b/apps/portal/src/app/account/support/new/page.tsx
@@ -1,5 +1,5 @@
import { NewSupportCaseView } from "@/features/support";
-export default function NewSupportCasePage() {
+export default function AccountNewSupportCasePage() {
return
;
}
diff --git a/apps/portal/src/app/(authenticated)/support/cases/page.tsx b/apps/portal/src/app/account/support/page.tsx
similarity index 65%
rename from apps/portal/src/app/(authenticated)/support/cases/page.tsx
rename to apps/portal/src/app/account/support/page.tsx
index 54a27c29..41ef7fa3 100644
--- a/apps/portal/src/app/(authenticated)/support/cases/page.tsx
+++ b/apps/portal/src/app/account/support/page.tsx
@@ -1,5 +1,5 @@
import { SupportCasesView } from "@/features/support";
-export default function SupportCasesPage() {
+export default function AccountSupportPage() {
return
;
}
diff --git a/apps/portal/src/components/organisms/AppShell/AppShell.tsx b/apps/portal/src/components/organisms/AppShell/AppShell.tsx
index eb3c2a03..4c04191f 100644
--- a/apps/portal/src/components/organisms/AppShell/AppShell.tsx
+++ b/apps/portal/src/components/organisms/AppShell/AppShell.tsx
@@ -67,9 +67,10 @@ export function AppShell({ children }: AppShellProps) {
useEffect(() => {
if (hasCheckedAuth && !isAuthenticated && !loading) {
- router.push("/auth/login");
+ const destination = pathname || "/account";
+ router.push(`/auth/login?redirect=${encodeURIComponent(destination)}`);
}
- }, [hasCheckedAuth, isAuthenticated, loading, router]);
+ }, [hasCheckedAuth, isAuthenticated, loading, pathname, router]);
// Hydrate full profile once after auth so header name is consistent across pages
useEffect(() => {
@@ -97,10 +98,10 @@ export function AppShell({ children }: AppShellProps) {
useEffect(() => {
setExpandedItems(prev => {
const next = new Set(prev);
- if (pathname.startsWith("/subscriptions")) next.add("Subscriptions");
- if (pathname.startsWith("/billing")) next.add("Billing");
- if (pathname.startsWith("/support")) next.add("Support");
- if (pathname.startsWith("/account")) next.add("Account");
+ if (pathname.startsWith("/account/services")) next.add("My Services");
+ if (pathname.startsWith("/account/billing")) next.add("Billing");
+ if (pathname.startsWith("/account/support")) next.add("Support");
+ if (pathname.startsWith("/account/settings")) next.add("Settings");
const result = Array.from(next);
// Avoid state update if unchanged
if (result.length === prev.length && result.every(v => prev.includes(v))) return prev;
diff --git a/apps/portal/src/components/organisms/AppShell/Header.tsx b/apps/portal/src/components/organisms/AppShell/Header.tsx
index 4f1f1d30..e058caff 100644
--- a/apps/portal/src/components/organisms/AppShell/Header.tsx
+++ b/apps/portal/src/components/organisms/AppShell/Header.tsx
@@ -39,7 +39,7 @@ export const Header = memo(function Header({ onMenuClick, user, profileReady }:
diff --git a/apps/portal/src/components/organisms/AppShell/navigation.ts b/apps/portal/src/components/organisms/AppShell/navigation.ts
index a63a4f0c..1f7663e7 100644
--- a/apps/portal/src/components/organisms/AppShell/navigation.ts
+++ b/apps/portal/src/components/organisms/AppShell/navigation.ts
@@ -26,33 +26,33 @@ export interface NavigationItem {
}
export const baseNavigation: NavigationItem[] = [
- { name: "Dashboard", href: "/dashboard", icon: HomeIcon },
- { name: "Orders", href: "/orders", icon: ClipboardDocumentListIcon },
+ { name: "Dashboard", href: "/account", icon: HomeIcon },
+ { name: "Orders", href: "/account/orders", icon: ClipboardDocumentListIcon },
{
name: "Billing",
icon: CreditCardIcon,
children: [
- { name: "Invoices", href: "/billing/invoices" },
- { name: "Payment Methods", href: "/billing/payments" },
+ { name: "Invoices", href: "/account/billing/invoices" },
+ { name: "Payment Methods", href: "/account/billing/payments" },
],
},
{
- name: "Subscriptions",
+ name: "My Services",
icon: ServerIcon,
- children: [{ name: "All Subscriptions", href: "/subscriptions" }],
+ children: [{ name: "All Services", href: "/account/services" }],
},
- { name: "Catalog", href: "/catalog", icon: Squares2X2Icon },
+ { name: "Shop", href: "/shop", icon: Squares2X2Icon },
{
name: "Support",
icon: ChatBubbleLeftRightIcon,
children: [
- { name: "Cases", href: "/support/cases" },
- { name: "New Case", href: "/support/new" },
+ { name: "Cases", href: "/account/support" },
+ { name: "New Case", href: "/account/support/new" },
],
},
{
- name: "Account",
- href: "/account",
+ name: "Settings",
+ href: "/account/settings",
icon: UserIcon,
},
{ name: "Log out", href: "#", icon: ArrowRightStartOnRectangleIcon, isLogout: true },
@@ -64,17 +64,17 @@ export function computeNavigation(activeSubscriptions?: Subscription[]): Navigat
children: item.children ? [...item.children] : undefined,
}));
- const subIdx = nav.findIndex(n => n.name === "Subscriptions");
+ const subIdx = nav.findIndex(n => n.name === "My Services");
if (subIdx >= 0) {
const dynamicChildren = (activeSubscriptions || []).map(sub => ({
name: truncate(sub.productName || `Subscription ${sub.id}`, 28),
- href: `/subscriptions/${sub.id}`,
+ href: `/account/services/${sub.id}`,
tooltip: sub.productName || `Subscription ${sub.id}`,
}));
nav[subIdx] = {
...nav[subIdx],
- children: [{ name: "All Subscriptions", href: "/subscriptions" }, ...dynamicChildren],
+ children: [{ name: "All Services", href: "/account/services" }, ...dynamicChildren],
};
}
diff --git a/apps/portal/src/components/templates/CatalogShell/CatalogShell.tsx b/apps/portal/src/components/templates/CatalogShell/CatalogShell.tsx
index e4e6eec1..cdfbe46b 100644
--- a/apps/portal/src/components/templates/CatalogShell/CatalogShell.tsx
+++ b/apps/portal/src/components/templates/CatalogShell/CatalogShell.tsx
@@ -5,15 +5,64 @@
* Extends the PublicShell with catalog navigation tabs.
*/
+"use client";
+
import type { ReactNode } from "react";
+import { useEffect } from "react";
import Link from "next/link";
import { Logo } from "@/components/atoms/logo";
+import { useAuthStore } from "@/features/auth/services/auth.store";
export interface CatalogShellProps {
children: ReactNode;
}
+export function CatalogNav() {
+ return (
+
+
+
+
+ All Services
+
+
+ Internet
+
+
+ SIM
+
+
+ VPN
+
+
+
+
+ );
+}
+
export function CatalogShell({ children }: CatalogShellProps) {
+ const isAuthenticated = useAuthStore(state => state.isAuthenticated);
+ const hasCheckedAuth = useAuthStore(state => state.hasCheckedAuth);
+ const checkAuth = useAuthStore(state => state.checkAuth);
+
+ useEffect(() => {
+ if (!hasCheckedAuth) {
+ void checkAuth();
+ }
+ }, [checkAuth, hasCheckedAuth]);
+
return (
{/* Subtle background pattern */}
@@ -34,39 +83,11 @@ export function CatalogShell({ children }: CatalogShellProps) {
Assist Solutions
- Customer Portal
+ Account Portal
- {/* Catalog Navigation */}
-
-
- All Services
-
-
- Internet
-
-
- SIM
-
-
- VPN
-
-
-
{/* Right side actions */}
Support
-
- Sign in
-
+ {isAuthenticated ? (
+
+ My Account
+
+ ) : (
+
+ Sign in
+
+ )}
+
+
{children}
diff --git a/apps/portal/src/components/templates/CatalogShell/index.ts b/apps/portal/src/components/templates/CatalogShell/index.ts
index a38391c3..4abbb86e 100644
--- a/apps/portal/src/components/templates/CatalogShell/index.ts
+++ b/apps/portal/src/components/templates/CatalogShell/index.ts
@@ -1,2 +1,2 @@
-export { CatalogShell } from "./CatalogShell";
+export { CatalogNav, CatalogShell } from "./CatalogShell";
export type { CatalogShellProps } from "./CatalogShell";
diff --git a/apps/portal/src/components/templates/PublicShell/PublicShell.tsx b/apps/portal/src/components/templates/PublicShell/PublicShell.tsx
index c1e2161e..a85bd1a1 100644
--- a/apps/portal/src/components/templates/PublicShell/PublicShell.tsx
+++ b/apps/portal/src/components/templates/PublicShell/PublicShell.tsx
@@ -1,12 +1,32 @@
+/**
+ * PublicShell
+ *
+ * Shared shell for public-facing pages with an auth-aware header.
+ */
+
+"use client";
+
import type { ReactNode } from "react";
+import { useEffect } from "react";
import Link from "next/link";
import { Logo } from "@/components/atoms/logo";
+import { useAuthStore } from "@/features/auth/services/auth.store";
export interface PublicShellProps {
children: ReactNode;
}
export function PublicShell({ children }: PublicShellProps) {
+ const isAuthenticated = useAuthStore(state => state.isAuthenticated);
+ const hasCheckedAuth = useAuthStore(state => state.hasCheckedAuth);
+ const checkAuth = useAuthStore(state => state.checkAuth);
+
+ useEffect(() => {
+ if (!hasCheckedAuth) {
+ void checkAuth();
+ }
+ }, [checkAuth, hasCheckedAuth]);
+
return (
{/* Subtle background pattern */}
@@ -26,7 +46,7 @@ export function PublicShell({ children }: PublicShellProps) {
Assist Solutions
- Customer Portal
+ Account Portal
@@ -44,12 +64,21 @@ export function PublicShell({ children }: PublicShellProps) {
>
Support
-
- Sign in
-
+ {isAuthenticated ? (
+
+ My Account
+
+ ) : (
+
+ Sign in
+
+ )}
diff --git a/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx b/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx
index bc6486ce..8efc594b 100644
--- a/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx
+++ b/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx
@@ -27,7 +27,7 @@ import { ReviewStep } from "./steps/ReviewStep";
* - phoneCountryCode: Separate field for country code input
* - address: Required addressFormSchema (domain schema makes it optional)
*/
-const signupFormBaseSchema = signupInputSchema.extend({
+const signupFormBaseSchema = signupInputSchema.omit({ sfNumber: true }).extend({
confirmPassword: z.string().min(1, "Please confirm your password"),
phoneCountryCode: z.string().regex(/^\+\d{1,4}$/, "Enter a valid country code (e.g., +81)"),
address: addressFormSchema,
@@ -75,16 +75,7 @@ const STEPS = [
] as const;
const STEP_FIELD_KEYS: Record<(typeof STEPS)[number]["key"], Array
> = {
- account: [
- "sfNumber",
- "firstName",
- "lastName",
- "email",
- "phone",
- "phoneCountryCode",
- "dateOfBirth",
- "gender",
- ],
+ account: ["firstName", "lastName", "email", "phone", "phoneCountryCode", "dateOfBirth", "gender"],
address: ["address"],
password: ["password", "confirmPassword"],
review: ["acceptTerms"],
@@ -92,7 +83,6 @@ const STEP_FIELD_KEYS: Record<(typeof STEPS)[number]["key"], Array = {
account: signupFormBaseSchema.pick({
- sfNumber: true,
firstName: true,
lastName: true,
email: true,
@@ -130,7 +120,6 @@ export function SignupForm({ onSuccess, onError, className = "" }: SignupFormPro
const form = useZodForm({
schema: signupFormSchema,
initialValues: {
- sfNumber: "",
firstName: "",
lastName: "",
email: "",
diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx b/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx
index b5b2ffc9..9fb9af15 100644
--- a/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx
+++ b/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx
@@ -1,5 +1,5 @@
/**
- * Account Step - Customer number and contact info
+ * Account Step - Contact info
*/
"use client";
@@ -10,7 +10,6 @@ import { FormField } from "@/components/molecules/FormField/FormField";
interface AccountStepProps {
form: {
values: {
- sfNumber: string;
firstName: string;
lastName: string;
email: string;
@@ -33,24 +32,6 @@ export function AccountStep({ form }: AccountStepProps) {
return (
- {/* Customer Number - Highlighted */}
-
-
- setValue("sfNumber", e.target.value)}
- onBlur={() => setTouchedField("sfNumber")}
- placeholder="e.g., AST-123456"
- autoFocus
- />
-
-
-
{/* Name Fields */}
@@ -61,6 +42,7 @@ export function AccountStep({ form }: AccountStepProps) {
onBlur={() => setTouchedField("firstName")}
placeholder="Taro"
autoComplete="given-name"
+ autoFocus
/>
diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx b/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx
index 9dcff4f4..2811b7ad 100644
--- a/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx
+++ b/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx
@@ -68,7 +68,6 @@ interface ReviewStepProps {
email: string;
phone: string;
phoneCountryCode: string;
- sfNumber: string;
company?: string;
dateOfBirth?: string;
gender?: "male" | "female" | "other";
@@ -116,10 +115,6 @@ export function ReviewStep({ form }: ReviewStepProps) {
Account Summary
-
-
Customer Number
- {values.sfNumber}
-
Name
diff --git a/apps/portal/src/features/auth/utils/route-protection.ts b/apps/portal/src/features/auth/utils/route-protection.ts
index 33408eac..7988c717 100644
--- a/apps/portal/src/features/auth/utils/route-protection.ts
+++ b/apps/portal/src/features/auth/utils/route-protection.ts
@@ -1,8 +1,8 @@
import type { ReadonlyURLSearchParams } from "next/navigation";
export function getPostLoginRedirect(searchParams: ReadonlyURLSearchParams): string {
- const dest = searchParams.get("redirect") || "/dashboard";
+ const dest = searchParams.get("redirect") || "/account";
// prevent open redirects
- if (dest.startsWith("http://") || dest.startsWith("https://")) return "/dashboard";
+ if (dest.startsWith("http://") || dest.startsWith("https://")) return "/account";
return dest;
}
diff --git a/apps/portal/src/features/auth/views/LinkWhmcsView.tsx b/apps/portal/src/features/auth/views/LinkWhmcsView.tsx
index 8d63a4e7..c92620c4 100644
--- a/apps/portal/src/features/auth/views/LinkWhmcsView.tsx
+++ b/apps/portal/src/features/auth/views/LinkWhmcsView.tsx
@@ -44,7 +44,7 @@ export function LinkWhmcsView() {
if (result.needsPasswordSet) {
router.push(`/auth/set-password?email=${encodeURIComponent(result.user.email)}`);
} else {
- router.push("/dashboard");
+ router.push("/account");
}
}}
/>
@@ -83,7 +83,7 @@ export function LinkWhmcsView() {
Need help?{" "}
-
+
Contact support
diff --git a/apps/portal/src/features/auth/views/SetPasswordView.tsx b/apps/portal/src/features/auth/views/SetPasswordView.tsx
index 43519b5a..0d78864e 100644
--- a/apps/portal/src/features/auth/views/SetPasswordView.tsx
+++ b/apps/portal/src/features/auth/views/SetPasswordView.tsx
@@ -20,7 +20,7 @@ function SetPasswordContent() {
const handlePasswordSetSuccess = () => {
// Redirect to dashboard after successful password setup
- router.push("/dashboard");
+ router.push("/account");
};
if (!email) {
diff --git a/apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx b/apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx
index e446e857..ec19e8c7 100644
--- a/apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx
+++ b/apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx
@@ -91,7 +91,7 @@ const BillingSummary = forwardRef(
{!compact && (
View All
@@ -158,7 +158,7 @@ const BillingSummary = forwardRef(
{compact && (
View All Invoices
diff --git a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx
index 4ebe2837..aba4a124 100644
--- a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx
+++ b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx
@@ -104,7 +104,7 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
if (isLinked) {
return (
-
+
{itemContent}
);
diff --git a/apps/portal/src/features/billing/components/InvoiceItemRow.tsx b/apps/portal/src/features/billing/components/InvoiceItemRow.tsx
index fe32456e..76939ed6 100644
--- a/apps/portal/src/features/billing/components/InvoiceItemRow.tsx
+++ b/apps/portal/src/features/billing/components/InvoiceItemRow.tsx
@@ -25,7 +25,7 @@ export function InvoiceItemRow({
? "border-blue-200 bg-blue-50 hover:bg-blue-100 cursor-pointer hover:shadow-sm"
: "border-gray-200 bg-gray-50"
}`}
- onClick={serviceId ? () => router.push(`/subscriptions/${serviceId}`) : undefined}
+ onClick={serviceId ? () => router.push(`/account/services/${serviceId}`) : undefined}
>
-
+
← Back to invoices
@@ -105,8 +105,8 @@ export function InvoiceDetailContainer() {
title={`Invoice #${invoice.id}`}
description="Invoice details and actions"
breadcrumbs={[
- { label: "Billing", href: "/billing/invoices" },
- { label: "Invoices", href: "/billing/invoices" },
+ { label: "Billing", href: "/account/billing/invoices" },
+ { label: "Invoices", href: "/account/billing/invoices" },
{ label: `#${invoice.id}` },
]}
>
diff --git a/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx b/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx
index 72ab73c1..69533ce3 100644
--- a/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx
+++ b/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx
@@ -20,7 +20,7 @@ interface InternetPlanCardProps {
installations: InternetInstallationCatalogItem[];
disabled?: boolean;
disabledReason?: string;
- /** Override the default configure href (default: /catalog/internet/configure?plan=...) */
+ /** Override the default configure href (default: /shop/internet/configure?plan=...) */
configureHref?: string;
}
@@ -205,7 +205,7 @@ export function InternetPlanCard({
const { resetInternetConfig, setInternetConfig } = useCatalogStore.getState();
resetInternetConfig();
setInternetConfig({ planSku: plan.sku, currentStep: 1 });
- const href = configureHref ?? `/catalog/internet/configure?plan=${plan.sku}`;
+ const href = configureHref ?? `/shop/internet/configure?plan=${plan.sku}`;
router.push(href);
}}
>
diff --git a/apps/portal/src/features/catalog/components/internet/configure/InternetConfigureContainer.tsx b/apps/portal/src/features/catalog/components/internet/configure/InternetConfigureContainer.tsx
index 335bbfc3..beb02db7 100644
--- a/apps/portal/src/features/catalog/components/internet/configure/InternetConfigureContainer.tsx
+++ b/apps/portal/src/features/catalog/components/internet/configure/InternetConfigureContainer.tsx
@@ -237,7 +237,7 @@ function PlanHeader({ plan }: { plan: InternetPlanCatalogItem }) {
}
diff --git a/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx b/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx
index 348bb154..f1d6d3d5 100644
--- a/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx
+++ b/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx
@@ -161,7 +161,7 @@ export function SimConfigureView({
Plan Not Found
The selected plan could not be found
-
+
← Return to SIM Plans
@@ -185,7 +185,7 @@ export function SimConfigureView({
icon={
}
>
-
+
diff --git a/apps/portal/src/features/catalog/components/vpn/VpnPlanCard.tsx b/apps/portal/src/features/catalog/components/vpn/VpnPlanCard.tsx
index bcea8cdc..eb25aae1 100644
--- a/apps/portal/src/features/catalog/components/vpn/VpnPlanCard.tsx
+++ b/apps/portal/src/features/catalog/components/vpn/VpnPlanCard.tsx
@@ -32,11 +32,11 @@ export function VpnPlanCard({ plan }: VpnPlanCardProps) {
}
>
- Configure Plan
+ Continue to Checkout
diff --git a/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts b/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts
index a4f8c725..b98438b1 100644
--- a/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts
+++ b/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts
@@ -75,7 +75,7 @@ export function useInternetConfigure(): UseInternetConfigureResult {
// Redirect if no plan selected
if (!urlPlanSku && !configState.planSku) {
- router.push("/catalog/internet");
+ router.push("/shop/internet");
}
}, [configState.planSku, paramsSignature, restoreFromParams, router, setConfig, urlPlanSku]);
diff --git a/apps/portal/src/features/catalog/hooks/useSimConfigure.ts b/apps/portal/src/features/catalog/hooks/useSimConfigure.ts
index 6980d91c..f694d96f 100644
--- a/apps/portal/src/features/catalog/hooks/useSimConfigure.ts
+++ b/apps/portal/src/features/catalog/hooks/useSimConfigure.ts
@@ -89,7 +89,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
// Redirect if no plan selected
if (!effectivePlanSku && !configState.planSku) {
- router.push("/catalog/sim");
+ router.push("/shop/sim");
}
}, [
configState.planSku,
diff --git a/apps/portal/src/features/catalog/views/CatalogHome.tsx b/apps/portal/src/features/catalog/views/CatalogHome.tsx
index f787861c..c148fcef 100644
--- a/apps/portal/src/features/catalog/views/CatalogHome.tsx
+++ b/apps/portal/src/features/catalog/views/CatalogHome.tsx
@@ -45,7 +45,7 @@ export function CatalogHomeView() {
"Multiple access modes",
"Professional installation",
]}
- href="/catalog/internet"
+ href="/shop/internet"
color="blue"
/>
diff --git a/apps/portal/src/features/catalog/views/InternetPlans.tsx b/apps/portal/src/features/catalog/views/InternetPlans.tsx
index 4095c17d..1d7b9b54 100644
--- a/apps/portal/src/features/catalog/views/InternetPlans.tsx
+++ b/apps/portal/src/features/catalog/views/InternetPlans.tsx
@@ -68,7 +68,7 @@ export function InternetPlansContainer() {
>
-
+
{/* Title + eligibility */}
@@ -112,7 +112,7 @@ export function InternetPlansContainer() {
icon={
}
>
-
+
contact us
@@ -197,7 +197,7 @@ export function InternetPlansContainer() {
We couldn't find any internet plans available for your location at this time.
-
+
@@ -72,7 +72,7 @@ export function PublicInternetPlansView() {
if (error) {
return (
-
+
{error instanceof Error ? error.message : "An unexpected error occurred"}
@@ -82,7 +82,7 @@ export function PublicInternetPlansView() {
return (
-
+
-
+
@@ -72,7 +72,7 @@ export function PublicSimPlansView() {
{errorMessage}
}
>
@@ -96,7 +96,7 @@ export function PublicSimPlansView() {
return (
-
+
-
+
-
+
}
>
-
+
{/* Title block */}
@@ -110,7 +110,7 @@ export function SimPlansContainer() {
{errorMessage}
}
>
@@ -140,7 +140,7 @@ export function SimPlansContainer() {
icon={
}
>
-
+
}
>
-
+
}
>
-
+
{
+ const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b));
+ return entries.map(([key, value]) => `${key}=${value}`).join("&");
+};
+
+const mapOrderTypeToCheckout = (orderType: OrderTypeValue): CheckoutOrderType => {
+ switch (orderType) {
+ case ORDER_TYPE.SIM:
+ return "SIM";
+ case ORDER_TYPE.VPN:
+ return "VPN";
+ case ORDER_TYPE.INTERNET:
+ case ORDER_TYPE.OTHER:
+ default:
+ return "INTERNET";
+ }
+};
+
+type CheckoutCartSummary = { items: CheckoutCart["items"]; totals: CheckoutCart["totals"] };
+
+const cartItemFromCheckoutCart = (
+ cart: CheckoutCartSummary,
+ orderType: OrderTypeValue
+): CartItem => {
+ const planItem = cart.items.find(item => item.itemType === "plan") ?? cart.items[0];
+ const planSku = planItem?.sku;
+ if (!planSku) {
+ throw new Error("Checkout cart did not include a plan. Please re-select your plan.");
+ }
+ const addonSkus = Array.from(
+ new Set(cart.items.map(item => item.sku).filter(sku => sku && sku !== planSku))
+ );
+
+ return {
+ orderType: mapOrderTypeToCheckout(orderType),
+ planSku,
+ planName: planItem?.name ?? planSku,
+ addonSkus,
+ configuration: {},
+ pricing: {
+ monthlyTotal: cart.totals.monthlyTotal,
+ oneTimeTotal: cart.totals.oneTimeTotal,
+ breakdown: cart.items.map(item => ({
+ label: item.name,
+ sku: item.sku,
+ monthlyPrice: item.monthlyPrice,
+ oneTimePrice: item.oneTimePrice,
+ quantity: item.quantity,
+ })),
+ },
+ };
+};
+
+export function CheckoutEntry() {
+ const searchParams = useSearchParams();
+ const paramsKey = useMemo(() => searchParams.toString(), [searchParams]);
+ const signature = useMemo(
+ () => signatureFromSearchParams(new URLSearchParams(paramsKey)),
+ [paramsKey]
+ );
+
+ const {
+ cartItem,
+ cartParamsSignature,
+ checkoutSessionId,
+ setCartItem,
+ setCartItemFromParams,
+ setCheckoutSession,
+ isCartStale,
+ clear,
+ } = useCheckoutStore();
+
+ const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
+ const [errorMessage, setErrorMessage] = useState(null);
+
+ useEffect(() => {
+ if (!paramsKey) {
+ return;
+ }
+
+ let mounted = true;
+ setStatus("loading");
+ setErrorMessage(null);
+
+ void (async () => {
+ try {
+ const snapshot = CheckoutParamsService.buildSnapshot(new URLSearchParams(paramsKey));
+ if (!snapshot.planReference) {
+ throw new Error("No plan selected. Please go back and select a plan.");
+ }
+
+ const session = await checkoutService.createSession(
+ snapshot.orderType,
+ snapshot.selections,
+ snapshot.configuration
+ );
+ if (!mounted) return;
+
+ const nextCartItem = cartItemFromCheckoutCart(session.cart, session.orderType);
+ setCartItemFromParams(nextCartItem, signature);
+ setCheckoutSession({ id: session.sessionId, expiresAt: session.expiresAt });
+ setStatus("idle");
+ } catch (error) {
+ if (!mounted) return;
+ setStatus("error");
+ setErrorMessage(error instanceof Error ? error.message : "Failed to load checkout");
+ }
+ })();
+
+ return () => {
+ mounted = false;
+ };
+ }, [paramsKey, setCartItemFromParams, setCheckoutSession, signature]);
+
+ useEffect(() => {
+ if (paramsKey) return;
+
+ if (isCartStale()) {
+ clear();
+ return;
+ }
+
+ if (!checkoutSessionId) {
+ return;
+ }
+
+ let mounted = true;
+ setStatus("loading");
+ setErrorMessage(null);
+
+ void (async () => {
+ try {
+ const session = await checkoutService.getSession(checkoutSessionId);
+ if (!mounted) return;
+ setCheckoutSession({ id: session.sessionId, expiresAt: session.expiresAt });
+ const nextCartItem = cartItemFromCheckoutCart(session.cart, session.orderType);
+ // Session-based entry: don't tie progress to URL params.
+ setCartItem(nextCartItem);
+ setStatus("idle");
+ } catch (error) {
+ if (!mounted) return;
+ setStatus("error");
+ setErrorMessage(error instanceof Error ? error.message : "Failed to load checkout");
+ }
+ })();
+
+ return () => {
+ mounted = false;
+ };
+ }, [checkoutSessionId, clear, isCartStale, paramsKey, setCartItem, setCheckoutSession]);
+
+ const shouldWaitForCart =
+ (Boolean(paramsKey) && (!cartItem || cartParamsSignature !== signature)) ||
+ (!paramsKey && Boolean(checkoutSessionId) && !cartItem);
+
+ if (status === "loading" && shouldWaitForCart) {
+ return (
+
+
+
+
Preparing your checkout…
+
+
+ );
+ }
+
+ if (status === "error") {
+ return (
+
+
+
+
{errorMessage}
+
+
+ Back to Shop
+
+
+ Contact support
+
+
+
+
+
+ );
+ }
+
+ if (!paramsKey && !checkoutSessionId) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/apps/portal/src/features/checkout/components/CheckoutErrorBoundary.tsx b/apps/portal/src/features/checkout/components/CheckoutErrorBoundary.tsx
index e828bf42..0b31edd9 100644
--- a/apps/portal/src/features/checkout/components/CheckoutErrorBoundary.tsx
+++ b/apps/portal/src/features/checkout/components/CheckoutErrorBoundary.tsx
@@ -58,7 +58,7 @@ export class CheckoutErrorBoundary extends Component {
If this problem persists, please{" "}
-
+
contact support
.
diff --git a/apps/portal/src/features/checkout/components/CheckoutShell.tsx b/apps/portal/src/features/checkout/components/CheckoutShell.tsx
index fe5216ac..7bbced29 100644
--- a/apps/portal/src/features/checkout/components/CheckoutShell.tsx
+++ b/apps/portal/src/features/checkout/components/CheckoutShell.tsx
@@ -1,9 +1,11 @@
"use client";
import type { ReactNode } from "react";
+import { useEffect } from "react";
import Link from "next/link";
import { Logo } from "@/components/atoms/logo";
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
+import { useAuthStore } from "@/features/auth/services/auth.store";
interface CheckoutShellProps {
children: ReactNode;
@@ -19,6 +21,15 @@ interface CheckoutShellProps {
* - Clean, focused design
*/
export function CheckoutShell({ children }: CheckoutShellProps) {
+ const hasCheckedAuth = useAuthStore(state => state.hasCheckedAuth);
+ const checkAuth = useAuthStore(state => state.checkAuth);
+
+ useEffect(() => {
+ if (!hasCheckedAuth) {
+ void checkAuth();
+ }
+ }, [checkAuth, hasCheckedAuth]);
+
return (
{/* Subtle background pattern */}
@@ -51,7 +62,7 @@ export function CheckoutShell({ children }: CheckoutShellProps) {
Secure Checkout
Need Help?
diff --git a/apps/portal/src/features/checkout/components/CheckoutWizard.tsx b/apps/portal/src/features/checkout/components/CheckoutWizard.tsx
index f8a117c8..b7b9d5ac 100644
--- a/apps/portal/src/features/checkout/components/CheckoutWizard.tsx
+++ b/apps/portal/src/features/checkout/components/CheckoutWizard.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useEffect } from "react";
import { useCheckoutStore } from "../stores/checkout.store";
import { CheckoutProgress } from "./CheckoutProgress";
import { OrderSummaryCard } from "./OrderSummaryCard";
@@ -9,6 +10,7 @@ import { AddressStep } from "./steps/AddressStep";
import { PaymentStep } from "./steps/PaymentStep";
import { ReviewStep } from "./steps/ReviewStep";
import type { CheckoutStep } from "@customer-portal/domain/checkout";
+import { useAuthSession } from "@/features/auth/services/auth.store";
/**
* CheckoutWizard - Main checkout flow orchestrator
@@ -17,8 +19,15 @@ import type { CheckoutStep } from "@customer-portal/domain/checkout";
* appropriate content based on current step.
*/
export function CheckoutWizard() {
+ const { isAuthenticated } = useAuthSession();
const { cartItem, currentStep, setCurrentStep, registrationComplete } = useCheckoutStore();
+ useEffect(() => {
+ if ((isAuthenticated || registrationComplete) && currentStep === "account") {
+ setCurrentStep("address");
+ }
+ }, [currentStep, isAuthenticated, registrationComplete, setCurrentStep]);
+
// Redirect if no cart
if (!cartItem) {
return
;
@@ -50,10 +59,8 @@ export function CheckoutWizard() {
};
// Determine effective step (skip account if already authenticated)
- const effectiveStep = registrationComplete && currentStep === "account" ? "address" : currentStep;
-
const renderStep = () => {
- switch (effectiveStep) {
+ switch (currentStep) {
case "account":
return
;
case "address":
@@ -71,7 +78,7 @@ export function CheckoutWizard() {
{/* Progress indicator */}
diff --git a/apps/portal/src/features/checkout/components/EmptyCartRedirect.tsx b/apps/portal/src/features/checkout/components/EmptyCartRedirect.tsx
index 9ceee79e..d8de2f68 100644
--- a/apps/portal/src/features/checkout/components/EmptyCartRedirect.tsx
+++ b/apps/portal/src/features/checkout/components/EmptyCartRedirect.tsx
@@ -8,7 +8,7 @@ import { Button } from "@/components/atoms/button";
/**
* EmptyCartRedirect - Shown when checkout is accessed without a cart
*
- * Redirects to catalog after a short delay, or user can click to go immediately.
+ * Redirects to shop after a short delay, or user can click to go immediately.
*/
export function EmptyCartRedirect() {
const router = useRouter();
@@ -29,13 +29,13 @@ export function EmptyCartRedirect() {
Your cart is empty
- Browse our catalog to find the perfect plan for your needs.
+ Browse our services to find the perfect plan for your needs.
- Browse Catalog
+ Browse Services
- Redirecting to catalog in a few seconds...
+ Redirecting to the shop in a few seconds...
diff --git a/apps/portal/src/features/checkout/components/OrderConfirmation.tsx b/apps/portal/src/features/checkout/components/OrderConfirmation.tsx
index bccb76e5..144ff0d7 100644
--- a/apps/portal/src/features/checkout/components/OrderConfirmation.tsx
+++ b/apps/portal/src/features/checkout/components/OrderConfirmation.tsx
@@ -84,10 +84,10 @@ export function OrderConfirmation() {
{/* Actions */}
-
+
Go to Dashboard
-
+
View Orders
@@ -95,7 +95,7 @@ export function OrderConfirmation() {
{/* Support Link */}
Have questions?{" "}
-
+
Contact Support
diff --git a/apps/portal/src/features/checkout/components/steps/AccountStep.tsx b/apps/portal/src/features/checkout/components/steps/AccountStep.tsx
index 20111f68..a106122d 100644
--- a/apps/portal/src/features/checkout/components/steps/AccountStep.tsx
+++ b/apps/portal/src/features/checkout/components/steps/AccountStep.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useCallback } from "react";
+import { useEffect, useState, useCallback } from "react";
import { z } from "zod";
import { useCheckoutStore } from "../../stores/checkout.store";
import { Button, Input } from "@/components/atoms";
@@ -8,6 +8,7 @@ import { FormField } from "@/components/molecules/FormField/FormField";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { UserIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
import { useZodForm } from "@/hooks/useZodForm";
+import { useAuthSession, useAuthStore } from "@/features/auth/services/auth.store";
import {
emailSchema,
passwordSchema,
@@ -39,6 +40,7 @@ type AccountFormData = z.infer
;
* Allows new customers to enter their info or existing customers to sign in.
*/
export function AccountStep() {
+ const { isAuthenticated } = useAuthSession();
const {
guestInfo,
updateGuestInfo,
@@ -77,9 +79,13 @@ export function AccountStep() {
onSubmit: handleSubmit,
});
- // If already registered, skip to address
- if (registrationComplete) {
- setCurrentStep("address");
+ useEffect(() => {
+ if (isAuthenticated || registrationComplete) {
+ setCurrentStep("address");
+ }
+ }, [isAuthenticated, registrationComplete, setCurrentStep]);
+
+ if (isAuthenticated || registrationComplete) {
return null;
}
@@ -262,6 +268,7 @@ function SignInForm({
}) {
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
+ const login = useAuthStore(state => state.login);
const handleSubmit = useCallback(
async (data: { email: string; password: string }) => {
@@ -269,20 +276,11 @@ function SignInForm({
setError(null);
try {
- const response = await fetch("/api/auth/login", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(data),
- credentials: "include",
- });
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.error?.message || "Invalid email or password");
+ await login(data);
+ const userId = useAuthStore.getState().user?.id;
+ if (userId) {
+ setRegistrationComplete(userId);
}
-
- const result = await response.json();
- setRegistrationComplete(result.user?.id || result.id || "");
onSuccess();
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed");
@@ -290,7 +288,7 @@ function SignInForm({
setIsLoading(false);
}
},
- [onSuccess, setRegistrationComplete]
+ [login, onSuccess, setRegistrationComplete]
);
const form = useZodForm<{ email: string; password: string }>({
diff --git a/apps/portal/src/features/checkout/components/steps/AddressStep.tsx b/apps/portal/src/features/checkout/components/steps/AddressStep.tsx
index 8211f007..64fab094 100644
--- a/apps/portal/src/features/checkout/components/steps/AddressStep.tsx
+++ b/apps/portal/src/features/checkout/components/steps/AddressStep.tsx
@@ -2,12 +2,15 @@
import { useState, useCallback } from "react";
import { useCheckoutStore } from "../../stores/checkout.store";
+import { useAuthSession, useAuthStore } from "@/features/auth/services/auth.store";
import { Button, Input } from "@/components/atoms";
import { FormField } from "@/components/molecules/FormField/FormField";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { MapPinIcon, ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
import { addressFormSchema, type AddressFormData } from "@customer-portal/domain/customer";
import { useZodForm } from "@/hooks/useZodForm";
+import { apiClient } from "@/lib/api";
+import { checkoutRegisterResponseSchema } from "@customer-portal/domain/checkout";
/**
* AddressStep - Second step in checkout
@@ -15,6 +18,8 @@ import { useZodForm } from "@/hooks/useZodForm";
* Collects service/shipping address and triggers registration for new users.
*/
export function AddressStep() {
+ const { isAuthenticated } = useAuthSession();
+ const refreshUser = useAuthStore(state => state.refreshUser);
const {
address,
setAddress,
@@ -33,12 +38,18 @@ export function AddressStep() {
setAddress(data);
// If not yet registered, trigger registration
- if (!registrationComplete && guestInfo) {
+ const hasGuestInfo =
+ Boolean(guestInfo?.email) &&
+ Boolean(guestInfo?.firstName) &&
+ Boolean(guestInfo?.lastName) &&
+ Boolean(guestInfo?.phone) &&
+ Boolean(guestInfo?.phoneCountryCode) &&
+ Boolean(guestInfo?.password);
+
+ if (!isAuthenticated && !registrationComplete && hasGuestInfo && guestInfo) {
try {
- const response = await fetch("/api/checkout/register", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
+ const response = await apiClient.POST("/api/checkout/register", {
+ body: {
email: guestInfo.email,
firstName: guestInfo.firstName,
lastName: guestInfo.lastName,
@@ -47,17 +58,12 @@ export function AddressStep() {
password: guestInfo.password,
address: data,
acceptTerms: true,
- }),
- credentials: "include",
+ },
});
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.error?.message || errorData.message || "Registration failed");
- }
-
- const result = await response.json();
+ const result = checkoutRegisterResponseSchema.parse(response.data);
setRegistrationComplete(result.user.id);
+ await refreshUser();
} catch (error) {
setRegistrationError(error instanceof Error ? error.message : "Registration failed");
return;
@@ -66,7 +72,15 @@ export function AddressStep() {
setCurrentStep("payment");
},
- [guestInfo, registrationComplete, setAddress, setCurrentStep, setRegistrationComplete]
+ [
+ guestInfo,
+ isAuthenticated,
+ refreshUser,
+ registrationComplete,
+ setAddress,
+ setCurrentStep,
+ setRegistrationComplete,
+ ]
);
const form = useZodForm({
diff --git a/apps/portal/src/features/checkout/components/steps/PaymentStep.tsx b/apps/portal/src/features/checkout/components/steps/PaymentStep.tsx
index 7b8f7b8b..50e4bc4b 100644
--- a/apps/portal/src/features/checkout/components/steps/PaymentStep.tsx
+++ b/apps/portal/src/features/checkout/components/steps/PaymentStep.tsx
@@ -2,8 +2,11 @@
import { useState, useEffect, useCallback } from "react";
import { useCheckoutStore } from "../../stores/checkout.store";
+import { useAuthSession } from "@/features/auth/services/auth.store";
import { Button } from "@/components/atoms/button";
import { Spinner } from "@/components/atoms";
+import { apiClient } from "@/lib/api";
+import { ssoLinkResponseSchema } from "@customer-portal/domain/auth";
import {
CreditCardIcon,
ArrowLeftIcon,
@@ -18,6 +21,7 @@ import {
* Opens WHMCS SSO to add payment method and polls for completion.
*/
export function PaymentStep() {
+ const { isAuthenticated } = useAuthSession();
const { setPaymentVerified, paymentMethodVerified, setCurrentStep, registrationComplete } =
useCheckoutStore();
const [isWaiting, setIsWaiting] = useState(false);
@@ -27,11 +31,12 @@ export function PaymentStep() {
lastFour?: string;
} | null>(null);
+ const canCheckPayment = isAuthenticated || registrationComplete;
+
// Poll for payment method
const checkPaymentMethod = useCallback(async () => {
- if (!registrationComplete) {
- // Need to be registered first - show message
- setError("Please complete registration first");
+ if (!canCheckPayment) {
+ setError("Please complete account setup first");
return false;
}
@@ -63,7 +68,7 @@ export function PaymentStep() {
console.error("Error checking payment methods:", err);
return false;
}
- }, [registrationComplete, setPaymentVerified]);
+ }, [canCheckPayment, setPaymentVerified]);
// Check on mount and when returning focus
useEffect(() => {
@@ -97,7 +102,7 @@ export function PaymentStep() {
}, [isWaiting, checkPaymentMethod]);
const handleAddPayment = async () => {
- if (!registrationComplete) {
+ if (!canCheckPayment) {
setError("Please complete account setup first");
return;
}
@@ -107,19 +112,11 @@ export function PaymentStep() {
try {
// Get SSO link for payment methods
- const response = await fetch("/api/auth/sso-link", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ destination: "paymentmethods" }),
- credentials: "include",
+ const response = await apiClient.POST("/api/auth/sso-link", {
+ body: { destination: "index.php?rp=/account/paymentmethods" },
});
-
- if (!response.ok) {
- throw new Error("Failed to get payment portal link");
- }
-
- const data = await response.json();
- const url = data.data?.url ?? data.url;
+ const data = ssoLinkResponseSchema.parse(response.data);
+ const url = data.url;
if (url) {
window.open(url, "_blank", "noopener,noreferrer");
@@ -199,10 +196,10 @@ export function PaymentStep() {
We'll open our secure payment portal where you can add your credit card or other
payment method.
-
+
Add Payment Method
- {!registrationComplete && (
+ {!canCheckPayment && (
You need to complete registration first
diff --git a/apps/portal/src/features/checkout/components/steps/ReviewStep.tsx b/apps/portal/src/features/checkout/components/steps/ReviewStep.tsx
index 6e4bfb67..4f3921fc 100644
--- a/apps/portal/src/features/checkout/components/steps/ReviewStep.tsx
+++ b/apps/portal/src/features/checkout/components/steps/ReviewStep.tsx
@@ -3,6 +3,8 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useCheckoutStore } from "../../stores/checkout.store";
+import { useAuthSession } from "@/features/auth/services/auth.store";
+import { ordersService } from "@/features/orders/services/orders.service";
import { Button } from "@/components/atoms/button";
import {
ShieldCheckIcon,
@@ -21,8 +23,16 @@ import {
*/
export function ReviewStep() {
const router = useRouter();
- const { cartItem, guestInfo, address, paymentMethodVerified, setCurrentStep, clear } =
- useCheckoutStore();
+ const { user } = useAuthSession();
+ const {
+ cartItem,
+ guestInfo,
+ address,
+ paymentMethodVerified,
+ checkoutSessionId,
+ setCurrentStep,
+ clear,
+ } = useCheckoutStore();
const [termsAccepted, setTermsAccepted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -43,31 +53,17 @@ export function ReviewStep() {
setError(null);
try {
- // Submit order via API
- const response = await fetch("/api/orders", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- orderType: cartItem.orderType,
- skus: [cartItem.planSku, ...cartItem.addonSkus],
- configuration: cartItem.configuration,
- }),
- credentials: "include",
- });
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.error?.message || "Failed to submit order");
+ if (!checkoutSessionId) {
+ throw new Error("Checkout session expired. Please restart checkout from the shop.");
}
- const result = await response.json();
- const orderId = result.data?.orderId ?? result.orderId;
+ const result = await ordersService.createOrderFromCheckoutSession(checkoutSessionId);
// Clear checkout state
clear();
// Redirect to confirmation
- router.push(`/order/complete${orderId ? `?orderId=${orderId}` : ""}`);
+ router.push(`/order/complete?orderId=${encodeURIComponent(result.sfOrderId)}`);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to submit order");
setIsSubmitting(false);
@@ -106,9 +102,9 @@ export function ReviewStep() {
- {guestInfo?.firstName} {guestInfo?.lastName}
+ {guestInfo?.firstName || user?.firstname} {guestInfo?.lastName || user?.lastname}
-
{guestInfo?.email}
+
{guestInfo?.email || user?.email}
{/* Address */}
diff --git a/apps/portal/src/features/checkout/hooks/useCheckout.ts b/apps/portal/src/features/checkout/hooks/useCheckout.ts
deleted file mode 100644
index 7ce706c1..00000000
--- a/apps/portal/src/features/checkout/hooks/useCheckout.ts
+++ /dev/null
@@ -1,229 +0,0 @@
-"use client";
-
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { useSearchParams, useRouter } from "next/navigation";
-import { logger } from "@/lib/logger";
-import { ordersService } from "@/features/orders/services/orders.service";
-import { checkoutService } from "@/features/checkout/services/checkout.service";
-import { ACTIVE_INTERNET_SUBSCRIPTION_WARNING } from "@/features/checkout/constants";
-import { usePaymentMethods } from "@/features/billing/hooks/useBilling";
-import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh";
-import {
- createLoadingState,
- createSuccessState,
- createErrorState,
-} from "@customer-portal/domain/toolkit";
-import type { AsyncState } from "@customer-portal/domain/toolkit";
-import { useActiveSubscriptions } from "@/features/subscriptions/hooks/useSubscriptions";
-import {
- ORDER_TYPE,
- orderWithSkuValidationSchema,
- prepareOrderFromCart,
- type CheckoutCart,
-} from "@customer-portal/domain/orders";
-import { CheckoutParamsService } from "@/features/checkout/services/checkout-params.service";
-import { useAuthSession, useAuthStore } from "@/features/auth/services/auth.store";
-import { ZodError } from "zod";
-
-// Use domain Address type
-import type { Address } from "@customer-portal/domain/customer";
-
-export function useCheckout() {
- const params = useSearchParams();
- const router = useRouter();
- const { isAuthenticated } = useAuthSession();
-
- const [submitting, setSubmitting] = useState(false);
- const [addressConfirmed, setAddressConfirmed] = useState(false);
-
- const [checkoutState, setCheckoutState] = useState>({
- status: "loading",
- });
-
- // Load active subscriptions to enforce business rules client-side before submission
- const { data: activeSubs } = useActiveSubscriptions();
- const hasActiveInternetSubscription = useMemo(() => {
- if (!Array.isArray(activeSubs)) return false;
- return activeSubs.some(
- subscription =>
- String(subscription.groupName || subscription.productName || "")
- .toLowerCase()
- .includes("internet") && String(subscription.status || "").toLowerCase() === "active"
- );
- }, [activeSubs]);
- const [activeInternetWarning, setActiveInternetWarning] = useState(null);
-
- const {
- data: paymentMethods,
- isLoading: paymentMethodsLoading,
- error: paymentMethodsError,
- refetch: refetchPaymentMethods,
- } = usePaymentMethods();
-
- const paymentRefresh = usePaymentRefresh({
- refetch: refetchPaymentMethods,
- attachFocusListeners: true,
- });
-
- const paramsKey = params.toString();
- const checkoutSnapshot = CheckoutParamsService.buildSnapshot(new URLSearchParams(paramsKey));
- const { orderType, warnings } = checkoutSnapshot;
-
- const lastWarningSignature = useRef(null);
-
- useEffect(() => {
- if (warnings.length === 0) {
- return;
- }
-
- const signature = warnings.join("|");
- if (signature === lastWarningSignature.current) {
- return;
- }
-
- lastWarningSignature.current = signature;
- warnings.forEach(message => {
- logger.warn("Checkout parameter warning", { message });
- });
- }, [warnings]);
-
- useEffect(() => {
- if (orderType !== ORDER_TYPE.INTERNET || !hasActiveInternetSubscription) {
- setActiveInternetWarning(null);
- return;
- }
-
- setActiveInternetWarning(ACTIVE_INTERNET_SUBSCRIPTION_WARNING);
- }, [orderType, hasActiveInternetSubscription]);
-
- useEffect(() => {
- // Wait for authentication before building cart
- if (!isAuthenticated) {
- return;
- }
-
- let mounted = true;
-
- void (async () => {
- const snapshot = CheckoutParamsService.buildSnapshot(new URLSearchParams(paramsKey));
- const {
- orderType: snapshotOrderType,
- selections,
- configuration,
- planReference: snapshotPlan,
- } = snapshot;
-
- try {
- setCheckoutState(createLoadingState());
-
- if (!snapshotPlan) {
- throw new Error("No plan selected. Please go back and select a plan.");
- }
-
- // Build cart using BFF service
- const cart = await checkoutService.buildCart(snapshotOrderType, selections, configuration);
-
- if (!mounted) return;
-
- setCheckoutState(createSuccessState(cart));
- } catch (error) {
- if (mounted) {
- const reason = error instanceof Error ? error.message : "Failed to load checkout data";
- setCheckoutState(createErrorState(new Error(reason)));
- }
- }
- })();
-
- return () => {
- mounted = false;
- };
- }, [isAuthenticated, paramsKey]);
-
- const handleSubmitOrder = useCallback(async () => {
- try {
- setSubmitting(true);
- if (checkoutState.status !== "success") {
- throw new Error("Checkout data not loaded");
- }
-
- const cart = checkoutState.data;
-
- // Debug logging to check cart contents
- console.log("[DEBUG] Cart data:", cart);
- console.log("[DEBUG] Cart items:", cart.items);
-
- // Validate cart before submission
- await checkoutService.validateCart(cart);
-
- // Use domain helper to prepare order data
- // This encapsulates SKU extraction and payload formatting
- const orderData = prepareOrderFromCart(cart, orderType);
-
- console.log("[DEBUG] Extracted SKUs from cart:", orderData.skus);
-
- const currentUserId = useAuthStore.getState().user?.id;
- if (currentUserId) {
- try {
- orderWithSkuValidationSchema.parse({
- ...orderData,
- userId: currentUserId,
- });
- } catch (validationError) {
- if (validationError instanceof ZodError) {
- const firstIssue = validationError.issues.at(0);
- throw new Error(firstIssue?.message || "Order contains invalid data");
- }
- throw validationError;
- }
- }
-
- const response = await ordersService.createOrder(orderData);
- router.push(`/orders/${response.sfOrderId}?status=success`);
- } catch (error) {
- let errorMessage = "Order submission failed";
- if (error instanceof Error) errorMessage = error.message;
- setCheckoutState(createErrorState(new Error(errorMessage)));
- } finally {
- setSubmitting(false);
- }
- }, [checkoutState, orderType, router]);
-
- const confirmAddress = useCallback((address?: Address) => {
- setAddressConfirmed(true);
- void address;
- }, []);
-
- const markAddressIncomplete = useCallback(() => {
- setAddressConfirmed(false);
- }, []);
-
- const navigateBackToConfigure = useCallback(() => {
- // State is already persisted in Zustand store
- // Just need to restore params and navigate
- const urlParams = new URLSearchParams(paramsKey);
- urlParams.delete("type"); // Remove type param as it's not needed
-
- const configureUrl =
- orderType === ORDER_TYPE.INTERNET
- ? `/shop/internet/configure?${urlParams.toString()}`
- : `/shop/sim/configure?${urlParams.toString()}`;
-
- router.push(configureUrl);
- }, [orderType, paramsKey, router]);
-
- return {
- checkoutState,
- submitting,
- orderType,
- addressConfirmed,
- paymentMethods,
- paymentMethodsLoading,
- paymentMethodsError,
- paymentRefresh,
- confirmAddress,
- markAddressIncomplete,
- handleSubmitOrder,
- navigateBackToConfigure,
- activeInternetWarning,
- } as const;
-}
diff --git a/apps/portal/src/features/checkout/services/checkout-api.service.ts b/apps/portal/src/features/checkout/services/checkout-api.service.ts
deleted file mode 100644
index 0fbd7246..00000000
--- a/apps/portal/src/features/checkout/services/checkout-api.service.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- * Checkout API Service
- *
- * Handles API calls for checkout flow.
- */
-
-import type { CartItem } from "@customer-portal/domain/checkout";
-import type { AddressFormData } from "@customer-portal/domain/customer";
-
-interface RegisterForCheckoutParams {
- guestInfo: {
- email: string;
- firstName: string;
- lastName: string;
- phone: string;
- phoneCountryCode: string;
- password: string;
- };
- address: AddressFormData;
-}
-
-interface CheckoutRegisterResult {
- success: boolean;
- user: {
- id: string;
- email: string;
- firstname: string;
- lastname: string;
- };
- session: {
- expiresAt: string;
- refreshExpiresAt: string;
- };
- sfAccountNumber?: string;
-}
-
-export const checkoutApiService = {
- /**
- * Register a new user during checkout
- */
- async registerForCheckout(params: RegisterForCheckoutParams): Promise {
- const response = await fetch("/api/checkout/register", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- email: params.guestInfo.email,
- firstName: params.guestInfo.firstName,
- lastName: params.guestInfo.lastName,
- phone: params.guestInfo.phone,
- phoneCountryCode: params.guestInfo.phoneCountryCode,
- password: params.guestInfo.password,
- address: params.address,
- acceptTerms: true,
- }),
- credentials: "include",
- });
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.error?.message || errorData.message || "Registration failed");
- }
-
- return response.json();
- },
-
- /**
- * Check if current user has a valid payment method
- */
- async getPaymentStatus(): Promise<{ hasPaymentMethod: boolean }> {
- try {
- const response = await fetch("/api/checkout/payment-status", {
- credentials: "include",
- });
-
- if (!response.ok) {
- return { hasPaymentMethod: false };
- }
-
- return response.json();
- } catch {
- return { hasPaymentMethod: false };
- }
- },
-
- /**
- * Submit order
- */
- async submitOrder(cartItem: CartItem): Promise<{ orderId?: string }> {
- const response = await fetch("/api/orders", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- orderType: cartItem.orderType,
- skus: [cartItem.planSku, ...cartItem.addonSkus],
- configuration: cartItem.configuration,
- }),
- credentials: "include",
- });
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.error?.message || "Failed to submit order");
- }
-
- const result = await response.json();
- return {
- orderId: result.data?.orderId ?? result.orderId,
- };
- },
-};
diff --git a/apps/portal/src/features/checkout/services/checkout.service.ts b/apps/portal/src/features/checkout/services/checkout.service.ts
index b24f50ac..9c01b37d 100644
--- a/apps/portal/src/features/checkout/services/checkout.service.ts
+++ b/apps/portal/src/features/checkout/services/checkout.service.ts
@@ -7,6 +7,15 @@ import type {
} from "@customer-portal/domain/orders";
import type { ApiSuccessResponse } from "@customer-portal/domain/common";
+type CheckoutCartSummary = { items: CheckoutCart["items"]; totals: CheckoutCart["totals"] };
+
+type CheckoutSessionResponse = {
+ sessionId: string;
+ expiresAt: string;
+ orderType: OrderTypeValue;
+ cart: CheckoutCartSummary;
+};
+
export const checkoutService = {
/**
* Build checkout cart from order type and selections
@@ -31,6 +40,40 @@ export const checkoutService = {
return wrappedResponse.data;
},
+ async createSession(
+ orderType: OrderTypeValue,
+ selections: OrderSelections,
+ configuration?: OrderConfigurations
+ ): Promise {
+ const response = await apiClient.POST>(
+ "/api/checkout/session",
+ {
+ body: { orderType, selections, configuration },
+ }
+ );
+
+ const wrappedResponse = getDataOrThrow(response, "Failed to create checkout session");
+ if (!wrappedResponse.success) {
+ throw new Error("Failed to create checkout session");
+ }
+ return wrappedResponse.data;
+ },
+
+ async getSession(sessionId: string): Promise {
+ const response = await apiClient.GET>(
+ "/api/checkout/session/{sessionId}",
+ {
+ params: { path: { sessionId } },
+ }
+ );
+
+ const wrappedResponse = getDataOrThrow(response, "Failed to load checkout session");
+ if (!wrappedResponse.success) {
+ throw new Error("Failed to load checkout session");
+ }
+ return wrappedResponse.data;
+ },
+
/**
* Validate checkout cart
*/
diff --git a/apps/portal/src/features/checkout/stores/checkout.store.ts b/apps/portal/src/features/checkout/stores/checkout.store.ts
index ae36689f..e5c9b6e2 100644
--- a/apps/portal/src/features/checkout/stores/checkout.store.ts
+++ b/apps/portal/src/features/checkout/stores/checkout.store.ts
@@ -13,6 +13,9 @@ import type { AddressFormData } from "@customer-portal/domain/customer";
interface CheckoutState {
// Cart data
cartItem: CartItem | null;
+ cartParamsSignature: string | null;
+ checkoutSessionId: string | null;
+ checkoutSessionExpiresAt: string | null;
// Guest info (pre-registration)
guestInfo: Partial | null;
@@ -37,6 +40,8 @@ interface CheckoutState {
interface CheckoutActions {
// Cart actions
setCartItem: (item: CartItem) => void;
+ setCartItemFromParams: (item: CartItem, signature: string) => void;
+ setCheckoutSession: (session: { id: string; expiresAt: string }) => void;
clearCart: () => void;
// Guest info actions
@@ -71,6 +76,9 @@ const STEP_ORDER: CheckoutStep[] = ["account", "address", "payment", "review"];
const initialState: CheckoutState = {
cartItem: null,
+ cartParamsSignature: null,
+ checkoutSessionId: null,
+ checkoutSessionExpiresAt: null,
guestInfo: null,
address: null,
registrationComplete: false,
@@ -92,9 +100,43 @@ export const useCheckoutStore = create()(
cartUpdatedAt: Date.now(),
}),
+ setCartItemFromParams: (item: CartItem, signature: string) => {
+ const { cartParamsSignature, cartItem } = get();
+ const signatureChanged = cartParamsSignature !== signature;
+ const hasExistingCart = cartItem !== null || cartParamsSignature !== null;
+
+ if (signatureChanged && hasExistingCart) {
+ set(initialState);
+ }
+
+ if (!signatureChanged && cartItem) {
+ // Allow refreshing cart totals without resetting progress
+ set({
+ cartItem: item,
+ cartUpdatedAt: Date.now(),
+ });
+ return;
+ }
+
+ set({
+ cartItem: item,
+ cartParamsSignature: signature,
+ cartUpdatedAt: Date.now(),
+ });
+ },
+
+ setCheckoutSession: session =>
+ set({
+ checkoutSessionId: session.id,
+ checkoutSessionExpiresAt: session.expiresAt,
+ }),
+
clearCart: () =>
set({
cartItem: null,
+ cartParamsSignature: null,
+ checkoutSessionId: null,
+ checkoutSessionExpiresAt: null,
cartUpdatedAt: null,
}),
@@ -171,7 +213,16 @@ export const useCheckoutStore = create()(
storage: createJSONStorage(() => localStorage),
partialize: state => ({
// Persist only essential data
- cartItem: state.cartItem,
+ cartItem: state.cartItem
+ ? {
+ ...state.cartItem,
+ // Avoid persisting potentially sensitive configuration details.
+ configuration: {},
+ }
+ : null,
+ cartParamsSignature: state.cartParamsSignature,
+ checkoutSessionId: state.checkoutSessionId,
+ checkoutSessionExpiresAt: state.checkoutSessionExpiresAt,
guestInfo: state.guestInfo,
address: state.address,
currentStep: state.currentStep,
diff --git a/apps/portal/src/features/checkout/views/CheckoutContainer.tsx b/apps/portal/src/features/checkout/views/CheckoutContainer.tsx
deleted file mode 100644
index fbda24b7..00000000
--- a/apps/portal/src/features/checkout/views/CheckoutContainer.tsx
+++ /dev/null
@@ -1,369 +0,0 @@
-"use client";
-import { useCheckout } from "@/features/checkout/hooks/useCheckout";
-import { PageLayout } from "@/components/templates/PageLayout";
-import { SubCard } from "@/components/molecules/SubCard/SubCard";
-import { Button } from "@/components/atoms/button";
-import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
-import { PageAsync } from "@/components/molecules/AsyncBlock/AsyncBlock";
-import { InlineToast } from "@/components/atoms/inline-toast";
-import { StatusPill } from "@/components/atoms/status-pill";
-import { AddressConfirmation } from "@/features/catalog/components/base/AddressConfirmation";
-import { isLoading, isError, isSuccess } from "@customer-portal/domain/toolkit";
-import { ShieldCheckIcon, CreditCardIcon } from "@heroicons/react/24/outline";
-import type { PaymentMethod } from "@customer-portal/domain/payments";
-
-export function CheckoutContainer() {
- const {
- checkoutState,
- submitting,
- orderType,
- addressConfirmed,
- paymentMethods,
- paymentMethodsLoading,
- paymentMethodsError,
- paymentRefresh,
- confirmAddress,
- markAddressIncomplete,
- handleSubmitOrder,
- navigateBackToConfigure,
- activeInternetWarning,
- } = useCheckout();
-
- if (isLoading(checkoutState)) {
- return (
- }
- >
-
- <>>
-
-
- );
- }
-
- if (isError(checkoutState)) {
- return (
- }
- >
-
-
-
- {checkoutState.error.message}
-
- Go Back
-
-
-
-
-
- );
- }
-
- if (!isSuccess(checkoutState)) {
- return (
- }
- >
-
-
-
- Checkout data is not available
-
- Go Back
-
-
-
-
-
- );
- }
-
- const { items, totals } = checkoutState.data;
- const paymentMethodList = paymentMethods?.paymentMethods ?? [];
- const defaultPaymentMethod =
- paymentMethodList.find(method => method.isDefault) ?? paymentMethodList[0] ?? null;
- const paymentMethodDisplay = defaultPaymentMethod
- ? buildPaymentMethodDisplay(defaultPaymentMethod)
- : null;
-
- return (
- }
- >
-
-
-
- {activeInternetWarning && (
-
- {activeInternetWarning}
-
- )}
-
-
-
-
-
Confirm Details
-
-
-
-
-
-
-
}
- right={
- paymentMethods && paymentMethods.paymentMethods.length > 0 ? (
-
- ) : undefined
- }
- >
- {paymentMethodsLoading ? (
-
Checking payment methods...
- ) : paymentMethodsError ? (
-
-
- void paymentRefresh.triggerRefresh()}
- >
- Check Again
-
-
- Add Payment Method
-
-
-
- ) : paymentMethodList.length > 0 ? (
-
- {paymentMethodDisplay ? (
-
-
-
-
- Default payment method
-
-
- {paymentMethodDisplay.title}
-
- {paymentMethodDisplay.subtitle ? (
-
- {paymentMethodDisplay.subtitle}
-
- ) : null}
-
-
- Manage billing & payments
-
-
-
- ) : null}
-
- We securely charge your saved payment method after the order is approved. Need
- to make changes? Visit Billing & Payments.
-
-
- ) : (
-
-
- void paymentRefresh.triggerRefresh()}
- >
- Check Again
-
-
- Add Payment Method
-
-
-
- )}
-
-
-
-
-
-
-
-
-
Review & Submit
-
- You’re almost done. Confirm your details above, then submit your order. We’ll review and
- notify you when everything is ready.
-
-
-
What to expect
-
-
• Our team reviews your order and schedules setup if needed
-
• We may contact you to confirm details or availability
-
• We only charge your card after the order is approved
-
• You’ll receive confirmation and next steps by email
-
-
-
-
-
-
Estimated Total
-
-
- ¥{totals.monthlyTotal.toLocaleString()}/mo
-
- {totals.oneTimeTotal > 0 && (
-
- + ¥{totals.oneTimeTotal.toLocaleString()} one-time
-
- )}
-
-
-
-
-
-
-
- ← Back to Configuration
-
- {
- void handleSubmitOrder();
- }}
- disabled={
- submitting ||
- items.length === 0 ||
- !addressConfirmed ||
- paymentMethodsLoading ||
- !paymentMethods ||
- paymentMethods.paymentMethods.length === 0
- }
- isLoading={submitting}
- loadingText="Submitting…"
- >
- {!addressConfirmed
- ? "Confirm Installation Address"
- : paymentMethodsLoading
- ? "Verifying Payment Method…"
- : !paymentMethods || paymentMethods.paymentMethods.length === 0
- ? "Add Payment Method to Continue"
- : "Submit Order"}
-
-
-
-
- );
-}
-
-function buildPaymentMethodDisplay(method: PaymentMethod): { title: string; subtitle?: string } {
- const descriptor =
- method.cardType?.trim() ||
- method.bankName?.trim() ||
- method.description?.trim() ||
- method.gatewayName?.trim() ||
- "Saved payment method";
-
- const trimmedLastFour =
- typeof method.cardLastFour === "string" && method.cardLastFour.trim().length > 0
- ? method.cardLastFour.trim().slice(-4)
- : null;
-
- const headline =
- trimmedLastFour && method.type?.toLowerCase().includes("card")
- ? `${descriptor} · •••• ${trimmedLastFour}`
- : descriptor;
-
- const details = new Set();
-
- if (method.bankName && !headline.toLowerCase().includes(method.bankName.trim().toLowerCase())) {
- details.add(method.bankName.trim());
- }
-
- const expiry = normalizeExpiryLabel(method.expiryDate);
- if (expiry) {
- details.add(`Exp ${expiry}`);
- }
-
- if (!trimmedLastFour && method.cardLastFour && method.cardLastFour.trim().length > 0) {
- details.add(`Ends ${method.cardLastFour.trim().slice(-4)}`);
- }
-
- if (method.type?.toLowerCase().includes("bank") && method.description?.trim()) {
- details.add(method.description.trim());
- }
-
- const subtitle = details.size > 0 ? Array.from(details).join(" · ") : undefined;
- return { title: headline, subtitle };
-}
-
-function normalizeExpiryLabel(expiry?: string | null): string | null {
- if (!expiry) return null;
- const value = expiry.trim();
- if (!value) return null;
-
- if (/^\d{4}-\d{2}$/.test(value)) {
- const [year, month] = value.split("-");
- return `${month}/${year.slice(-2)}`;
- }
-
- if (/^\d{2}\/\d{4}$/.test(value)) {
- const [month, year] = value.split("/");
- return `${month}/${year.slice(-2)}`;
- }
-
- if (/^\d{2}\/\d{2}$/.test(value)) {
- return value;
- }
-
- const digits = value.replace(/\D/g, "");
-
- if (digits.length === 6) {
- const year = digits.slice(2, 4);
- const month = digits.slice(4, 6);
- return `${month}/${year}`;
- }
-
- if (digits.length === 4) {
- const month = digits.slice(0, 2);
- const year = digits.slice(2, 4);
- return `${month}/${year}`;
- }
-
- return value;
-}
-
-export default CheckoutContainer;
diff --git a/apps/portal/src/features/dashboard/components/QuickStats.tsx b/apps/portal/src/features/dashboard/components/QuickStats.tsx
index 5f6bfe68..1a9deab3 100644
--- a/apps/portal/src/features/dashboard/components/QuickStats.tsx
+++ b/apps/portal/src/features/dashboard/components/QuickStats.tsx
@@ -135,7 +135,7 @@ export function QuickStats({
icon={ServerIcon}
label="Active Services"
value={activeSubscriptions}
- href="/subscriptions"
+ href="/account/services"
tone="primary"
emptyText="No active services"
/>
@@ -143,7 +143,7 @@ export function QuickStats({
icon={ChatBubbleLeftRightIcon}
label="Open Support Cases"
value={openCases}
- href="/support/cases"
+ href="/account/support"
tone={openCases > 0 ? "warning" : "info"}
emptyText="No open cases"
/>
@@ -152,7 +152,7 @@ export function QuickStats({
icon={ClipboardDocumentListIcon}
label="Recent Orders"
value={recentOrders}
- href="/orders"
+ href="/account/orders"
tone="success"
emptyText="No recent orders"
/>
diff --git a/apps/portal/src/features/dashboard/components/TaskList.tsx b/apps/portal/src/features/dashboard/components/TaskList.tsx
index 36edbc6d..b9971344 100644
--- a/apps/portal/src/features/dashboard/components/TaskList.tsx
+++ b/apps/portal/src/features/dashboard/components/TaskList.tsx
@@ -52,18 +52,18 @@ function AllCaughtUp() {
{/* Quick action cards */}
-
Browse Catalog
+
Browse Services
@@ -74,7 +74,7 @@ function AllCaughtUp() {
diff --git a/apps/portal/src/features/dashboard/hooks/useDashboardTasks.ts b/apps/portal/src/features/dashboard/hooks/useDashboardTasks.ts
index 5569eb61..8c33fb37 100644
--- a/apps/portal/src/features/dashboard/hooks/useDashboardTasks.ts
+++ b/apps/portal/src/features/dashboard/hooks/useDashboardTasks.ts
@@ -82,7 +82,7 @@ function computeTasks({
title: isOverdue ? "Pay overdue invoice" : "Pay upcoming invoice",
description: `Invoice #${summary.nextInvoice.id} · ${formatCurrency(summary.nextInvoice.amount, { currency: summary.nextInvoice.currency })} · ${dueText}`,
actionLabel: "Pay now",
- detailHref: `/billing/invoices/${summary.nextInvoice.id}`,
+ detailHref: `/account/billing/invoices/${summary.nextInvoice.id}`,
requiresSsoAction: true,
tone: "critical",
icon: ExclamationCircleIcon,
@@ -103,7 +103,7 @@ function computeTasks({
title: "Add a payment method",
description: "Required to place orders and process invoices",
actionLabel: "Add method",
- detailHref: "/billing/payments",
+ detailHref: "/account/billing/payments",
requiresSsoAction: true,
tone: "warning",
icon: CreditCardIcon,
@@ -135,7 +135,7 @@ function computeTasks({
title: "Order in progress",
description: `${order.orderType || "Your"} order is ${statusText}`,
actionLabel: "View details",
- detailHref: `/orders/${order.id}`,
+ detailHref: `/account/orders/${order.id}`,
tone: "info",
icon: ClockIcon,
metadata: { orderId: order.id },
@@ -151,8 +151,8 @@ function computeTasks({
type: "onboarding",
title: "Start your first service",
description: "Browse our catalog and subscribe to internet, SIM, or VPN",
- actionLabel: "Browse catalog",
- detailHref: "/catalog",
+ actionLabel: "Browse services",
+ detailHref: "/shop",
tone: "neutral",
icon: SparklesIcon,
});
diff --git a/apps/portal/src/features/dashboard/utils/dashboard.utils.ts b/apps/portal/src/features/dashboard/utils/dashboard.utils.ts
index 3900aabd..d27c499d 100644
--- a/apps/portal/src/features/dashboard/utils/dashboard.utils.ts
+++ b/apps/portal/src/features/dashboard/utils/dashboard.utils.ts
@@ -38,12 +38,12 @@ export function getActivityNavigationPath(activity: Activity): string | null {
switch (activity.type) {
case "invoice_created":
case "invoice_paid":
- return `/billing/invoices/${activity.relatedId}`;
+ return `/account/billing/invoices/${activity.relatedId}`;
case "service_activated":
- return `/subscriptions/${activity.relatedId}`;
+ return `/account/services/${activity.relatedId}`;
case "case_created":
case "case_closed":
- return `/support/cases/${activity.relatedId}`;
+ return `/account/support/${activity.relatedId}`;
default:
return null;
}
diff --git a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
index 99394cb0..7fb25fb9 100644
--- a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
+++ b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
@@ -27,7 +27,7 @@ export function PublicLandingView() {
- Customer Portal
+ Account Portal
Manage your services, billing, and support in one place.
@@ -54,7 +54,7 @@ export function PublicLandingView() {
href="/shop"
className="inline-flex items-center justify-center gap-2 rounded-xl px-8 py-4 text-base font-semibold bg-primary text-primary-foreground hover:bg-primary-hover shadow-lg shadow-primary/30 hover:shadow-xl hover:shadow-primary/40 transition-all whitespace-nowrap"
>
- View Catalog
+ Shop Services
diff --git a/apps/portal/src/features/orders/services/orders.service.ts b/apps/portal/src/features/orders/services/orders.service.ts
index 17416ae5..7704549a 100644
--- a/apps/portal/src/features/orders/services/orders.service.ts
+++ b/apps/portal/src/features/orders/services/orders.service.ts
@@ -30,18 +30,27 @@ async function createOrder(payload: CreateOrderRequest): Promise<{ sfOrderId: st
);
return { sfOrderId: parsed.data.sfOrderId };
} catch (error) {
- log.error(
- "Order creation failed",
- error instanceof Error ? error : undefined,
- {
- orderType: body.orderType,
- skuCount: body.skus.length,
- }
- );
+ log.error("Order creation failed", error instanceof Error ? error : undefined, {
+ orderType: body.orderType,
+ skuCount: body.skus.length,
+ });
throw error;
}
}
+async function createOrderFromCheckoutSession(
+ checkoutSessionId: string
+): Promise<{ sfOrderId: string }> {
+ const response = await apiClient.POST("/api/orders/from-checkout-session", {
+ body: { checkoutSessionId },
+ });
+
+ const parsed = assertSuccess<{ sfOrderId: string; status: string; message: string }>(
+ response.data as DomainApiResponse<{ sfOrderId: string; status: string; message: string }>
+ );
+ return { sfOrderId: parsed.data.sfOrderId };
+}
+
async function getMyOrders(): Promise
{
const response = await apiClient.GET("/api/orders/user");
const data = Array.isArray(response.data) ? response.data : [];
@@ -68,6 +77,7 @@ async function getOrderById(
export const ordersService = {
createOrder,
+ createOrderFromCheckoutSession,
getMyOrders,
getOrderById,
} as const;
diff --git a/apps/portal/src/features/orders/views/OrderDetail.tsx b/apps/portal/src/features/orders/views/OrderDetail.tsx
index 70adf445..afc39a2d 100644
--- a/apps/portal/src/features/orders/views/OrderDetail.tsx
+++ b/apps/portal/src/features/orders/views/OrderDetail.tsx
@@ -275,7 +275,7 @@ export function OrderDetailContainer() {
title={data ? `${data.orderType} Service Order` : "Order Details"}
description={data ? `Order #${orderNumber}` : "Loading order details..."}
breadcrumbs={[
- { label: "Orders", href: "/orders" },
+ { label: "Orders", href: "/account/orders" },
{ label: data ? `Order #${orderNumber}` : "Order Details" },
]}
>
diff --git a/apps/portal/src/features/orders/views/OrdersList.tsx b/apps/portal/src/features/orders/views/OrdersList.tsx
index d34cb331..f6681c92 100644
--- a/apps/portal/src/features/orders/views/OrdersList.tsx
+++ b/apps/portal/src/features/orders/views/OrdersList.tsx
@@ -90,7 +90,7 @@ export function OrdersListContainer() {
icon={ }
title="No orders yet"
description="You haven't placed any orders yet."
- action={{ label: "Browse Catalog", onClick: () => router.push("/catalog") }}
+ action={{ label: "Browse Services", onClick: () => router.push("/shop") }}
/>
) : (
@@ -99,7 +99,7 @@ export function OrdersListContainer() {
router.push(`/orders/${order.id}`)}
+ onClick={() => router.push(`/account/orders/${order.id}`)}
/>
))}
diff --git a/apps/portal/src/features/sim-management/components/SimActions.tsx b/apps/portal/src/features/sim-management/components/SimActions.tsx
index c4dacb40..d421d254 100644
--- a/apps/portal/src/features/sim-management/components/SimActions.tsx
+++ b/apps/portal/src/features/sim-management/components/SimActions.tsx
@@ -149,7 +149,7 @@ export function SimActions({
onClick={() => {
setActiveInfo("topup");
try {
- router.push(`/subscriptions/${subscriptionId}/sim/top-up`);
+ router.push(`/account/services/${subscriptionId}/sim/top-up`);
} catch {
setShowTopUpModal(true);
}
@@ -177,7 +177,7 @@ export function SimActions({
onClick={() => {
setActiveInfo("changePlan");
try {
- router.push(`/subscriptions/${subscriptionId}/sim/change-plan`);
+ router.push(`/account/services/${subscriptionId}/sim/change-plan`);
} catch {
setShowChangePlanModal(true);
}
@@ -236,7 +236,7 @@ export function SimActions({
onClick={() => {
setActiveInfo("cancel");
try {
- router.push(`/subscriptions/${subscriptionId}/sim/cancel`);
+ router.push(`/account/services/${subscriptionId}/sim/cancel`);
} catch {
// Fallback to inline confirmation modal if navigation is unavailable
setShowCancelConfirm(true);
diff --git a/apps/portal/src/features/sim-management/components/SimManagementSection.tsx b/apps/portal/src/features/sim-management/components/SimManagementSection.tsx
index ae748958..fc3ba9d2 100644
--- a/apps/portal/src/features/sim-management/components/SimManagementSection.tsx
+++ b/apps/portal/src/features/sim-management/components/SimManagementSection.tsx
@@ -56,13 +56,13 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
const [error, setError] = useState
(null);
// Navigation handlers
- const navigateToTopUp = () => router.push(`/subscriptions/${subscriptionId}/sim/top-up`);
+ const navigateToTopUp = () => router.push(`/account/services/${subscriptionId}/sim/top-up`);
const navigateToChangePlan = () =>
- router.push(`/subscriptions/${subscriptionId}/sim/change-plan`);
- const navigateToReissue = () => router.push(`/subscriptions/${subscriptionId}/sim/reissue`);
- const navigateToCancel = () => router.push(`/subscriptions/${subscriptionId}/sim/cancel`);
+ router.push(`/account/services/${subscriptionId}/sim/change-plan`);
+ const navigateToReissue = () => router.push(`/account/services/${subscriptionId}/sim/reissue`);
+ const navigateToCancel = () => router.push(`/account/services/${subscriptionId}/sim/cancel`);
const navigateToCallHistory = () =>
- router.push(`/subscriptions/${subscriptionId}/sim/call-history`);
+ router.push(`/account/services/${subscriptionId}/sim/call-history`);
// Fetch subscription data
const { data: subscription } = useSubscription(subscriptionId);
diff --git a/apps/portal/src/features/subscriptions/components/SubscriptionTable/SubscriptionTable.tsx b/apps/portal/src/features/subscriptions/components/SubscriptionTable/SubscriptionTable.tsx
index ba0fe934..61251db1 100644
--- a/apps/portal/src/features/subscriptions/components/SubscriptionTable/SubscriptionTable.tsx
+++ b/apps/portal/src/features/subscriptions/components/SubscriptionTable/SubscriptionTable.tsx
@@ -101,7 +101,7 @@ export function SubscriptionTable({
if (onSubscriptionClick) {
onSubscriptionClick(subscription);
} else {
- router.push(`/subscriptions/${subscription.id}`);
+ router.push(`/account/services/${subscription.id}`);
}
},
[onSubscriptionClick, router]
diff --git a/apps/portal/src/features/subscriptions/containers/SimCancel.tsx b/apps/portal/src/features/subscriptions/containers/SimCancel.tsx
index f96ba73f..fa2b2ab3 100644
--- a/apps/portal/src/features/subscriptions/containers/SimCancel.tsx
+++ b/apps/portal/src/features/subscriptions/containers/SimCancel.tsx
@@ -108,7 +108,7 @@ export function SimCancelContainer() {
try {
await simActionsService.cancel(subscriptionId, { scheduledAt: runDate });
setMessage("Cancellation request submitted. You will receive a confirmation email.");
- setTimeout(() => router.push(`/subscriptions/${subscriptionId}#sim-management`), 1500);
+ setTimeout(() => router.push(`/account/services/${subscriptionId}#sim-management`), 1500);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to submit cancellation");
} finally {
@@ -120,7 +120,7 @@ export function SimCancelContainer() {
← Back to SIM Management
diff --git a/apps/portal/src/features/subscriptions/views/SimCallHistory.tsx b/apps/portal/src/features/subscriptions/views/SimCallHistory.tsx
index 01017416..5bc0cfc4 100644
--- a/apps/portal/src/features/subscriptions/views/SimCallHistory.tsx
+++ b/apps/portal/src/features/subscriptions/views/SimCallHistory.tsx
@@ -138,7 +138,7 @@ export function SimCallHistoryContainer() {
← Back to SIM Management
diff --git a/apps/portal/src/features/subscriptions/views/SimCancel.tsx b/apps/portal/src/features/subscriptions/views/SimCancel.tsx
index c64bc5b8..61780fa6 100644
--- a/apps/portal/src/features/subscriptions/views/SimCancel.tsx
+++ b/apps/portal/src/features/subscriptions/views/SimCancel.tsx
@@ -102,7 +102,7 @@ export function SimCancelContainer() {
comments: comments.trim() || undefined,
});
setMessage("Cancellation request submitted. You will receive a confirmation email.");
- setTimeout(() => router.push(`/subscriptions/${subscriptionId}#sim-management`), 2000);
+ setTimeout(() => router.push(`/account/services/${subscriptionId}#sim-management`), 2000);
} catch (e: unknown) {
setError(
process.env.NODE_ENV === "development"
@@ -125,8 +125,8 @@ export function SimCancelContainer() {
title="Cancel SIM"
description="Cancel your SIM subscription"
breadcrumbs={[
- { label: "Subscriptions", href: "/subscriptions" },
- { label: "SIM Management", href: `/subscriptions/${subscriptionId}#sim-management` },
+ { label: "Services", href: "/account/services" },
+ { label: "SIM Management", href: `/account/services/${subscriptionId}#sim-management` },
{ label: "Cancel SIM" },
]}
loading={loadingPreview}
@@ -136,7 +136,7 @@ export function SimCancelContainer() {
← Back to SIM Management
diff --git a/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx b/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx
index e2f2d5f8..ca7096e2 100644
--- a/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx
+++ b/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx
@@ -89,7 +89,7 @@ export function SimChangePlanContainer() {
← Back to SIM Management
@@ -241,7 +241,7 @@ export function SimChangePlanContainer() {
Cancel
diff --git a/apps/portal/src/features/subscriptions/views/SimReissue.tsx b/apps/portal/src/features/subscriptions/views/SimReissue.tsx
index 20c5a216..d411ff5c 100644
--- a/apps/portal/src/features/subscriptions/views/SimReissue.tsx
+++ b/apps/portal/src/features/subscriptions/views/SimReissue.tsx
@@ -108,7 +108,7 @@ export function SimReissueContainer() {
← Back to SIM Management
@@ -315,7 +315,7 @@ export function SimReissueContainer() {
Cancel
diff --git a/apps/portal/src/features/subscriptions/views/SimTopUp.tsx b/apps/portal/src/features/subscriptions/views/SimTopUp.tsx
index 4eda5bf1..bd11a826 100644
--- a/apps/portal/src/features/subscriptions/views/SimTopUp.tsx
+++ b/apps/portal/src/features/subscriptions/views/SimTopUp.tsx
@@ -68,7 +68,7 @@ export function SimTopUpContainer() {
← Back to SIM Management
@@ -151,7 +151,7 @@ export function SimTopUpContainer() {
Back
diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx
index 3236b02f..f432151a 100644
--- a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx
+++ b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx
@@ -72,7 +72,7 @@ export function SubscriptionDetailContainer() {
subscription ? `Service ID: ${subscription.serviceId}` : "View your subscription details"
}
breadcrumbs={[
- { label: "Subscriptions", href: "/subscriptions" },
+ { label: "Services", href: "/account/services" },
{ label: subscription?.productName ?? "Subscription" },
]}
loading={isLoading}
@@ -147,7 +147,7 @@ export function SubscriptionDetailContainer() {
{isSimService && (
}
- title="Subscriptions"
- description="Manage your active services and subscriptions"
+ title="Services"
+ description="Manage your active services"
actions={
-
- Order Services
+
+ Shop Services
}
>
diff --git a/apps/portal/src/features/support/views/NewSupportCaseView.tsx b/apps/portal/src/features/support/views/NewSupportCaseView.tsx
index 12032ca6..991cb24d 100644
--- a/apps/portal/src/features/support/views/NewSupportCaseView.tsx
+++ b/apps/portal/src/features/support/views/NewSupportCaseView.tsx
@@ -38,7 +38,7 @@ export function NewSupportCaseView() {
priority: formData.priority,
});
- router.push("/support/cases?created=true");
+ router.push("/account/support?created=true");
} catch (err) {
setError(
process.env.NODE_ENV === "development"
@@ -70,7 +70,7 @@ export function NewSupportCaseView() {
icon={ }
title="Create Support Case"
description="Get help from our support team"
- breadcrumbs={[{ label: "Support", href: "/support" }, { label: "Create Case" }]}
+ breadcrumbs={[{ label: "Support", href: "/account/support" }, { label: "Create Case" }]}
>
{/* AI Chat Suggestion */}
@@ -178,7 +178,7 @@ export function NewSupportCaseView() {
{/* Actions */}
Cancel
diff --git a/apps/portal/src/features/support/views/PublicSupportView.tsx b/apps/portal/src/features/support/views/PublicSupportView.tsx
index 0ce58281..49545f17 100644
--- a/apps/portal/src/features/support/views/PublicSupportView.tsx
+++ b/apps/portal/src/features/support/views/PublicSupportView.tsx
@@ -61,7 +61,7 @@ export function PublicSupportView() {
{/* Contact Options */}
@@ -125,7 +125,7 @@ export function PublicSupportView() {
Sign in
{" "}
- to access your dashboard and support tickets.
+ to access your dashboard and support cases.
diff --git a/apps/portal/src/features/support/views/SupportCaseDetailView.tsx b/apps/portal/src/features/support/views/SupportCaseDetailView.tsx
index b3200f86..ffad545d 100644
--- a/apps/portal/src/features/support/views/SupportCaseDetailView.tsx
+++ b/apps/portal/src/features/support/views/SupportCaseDetailView.tsx
@@ -30,8 +30,8 @@ export function SupportCaseDetailView({ caseId }: SupportCaseDetailViewProps) {
icon={
}
title="Case Not Found"
breadcrumbs={[
- { label: "Support", href: "/support" },
- { label: "Cases", href: "/support/cases" },
+ { label: "Support", href: "/account/support" },
+ { label: "Cases", href: "/account/support" },
{ label: "Not Found" },
]}
>
@@ -52,14 +52,14 @@ export function SupportCaseDetailView({ caseId }: SupportCaseDetailViewProps) {
error={pageError}
onRetry={() => void refetch()}
breadcrumbs={[
- { label: "Support", href: "/support" },
- { label: "Cases", href: "/support/cases" },
+ { label: "Support", href: "/account/support" },
+ { label: "Cases", href: "/account/support" },
{ label: supportCase ? `#${supportCase.caseNumber}` : "..." },
]}
actions={
}
>
diff --git a/apps/portal/src/features/support/views/SupportCasesView.tsx b/apps/portal/src/features/support/views/SupportCasesView.tsx
index 5a13d5ec..9d37ba62 100644
--- a/apps/portal/src/features/support/views/SupportCasesView.tsx
+++ b/apps/portal/src/features/support/views/SupportCasesView.tsx
@@ -90,9 +90,9 @@ export function SupportCasesView() {
loading={isLoading}
error={error}
onRetry={() => void refetch()}
- breadcrumbs={[{ label: "Support", href: "/support" }, { label: "Cases" }]}
+ breadcrumbs={[{ label: "Support", href: "/account/support" }, { label: "Cases" }]}
actions={
-
}>
+
}>
New Case
}
@@ -183,7 +183,7 @@ export function SupportCasesView() {
{cases.map(supportCase => (
router.push(`/support/cases/${supportCase.id}`)}
+ onClick={() => router.push(`/account/support/${supportCase.id}`)}
className="flex items-center gap-4 p-4 hover:bg-muted cursor-pointer transition-colors group"
>
{/* Status Icon */}
@@ -239,7 +239,7 @@ export function SupportCasesView() {
description="You haven't created any support cases yet. Need help? Create a new case."
action={{
label: "Create Case",
- onClick: () => router.push("/support/new"),
+ onClick: () => router.push("/account/support/new"),
}}
/>
diff --git a/apps/portal/src/features/support/views/SupportHomeView.tsx b/apps/portal/src/features/support/views/SupportHomeView.tsx
index f82c0c49..39389817 100644
--- a/apps/portal/src/features/support/views/SupportHomeView.tsx
+++ b/apps/portal/src/features/support/views/SupportHomeView.tsx
@@ -65,7 +65,7 @@ export function SupportHomeView() {
Our team typically responds within 24 hours.
-
+
New Case
@@ -93,7 +93,7 @@ export function SupportHomeView() {
{summary.total > 0 && (
View all
@@ -107,7 +107,7 @@ export function SupportHomeView() {
{recentCases.map(supportCase => (
router.push(`/support/cases/${supportCase.id}`)}
+ onClick={() => router.push(`/account/support/${supportCase.id}`)}
className="flex items-center gap-4 p-4 hover:bg-muted cursor-pointer transition-colors group"
>
{getCaseStatusIcon(supportCase.status)}
@@ -139,7 +139,7 @@ export function SupportHomeView() {
description="Need help? Start a chat with our AI assistant or create a support case."
action={{
label: "Create Case",
- onClick: () => router.push("/support/new"),
+ onClick: () => router.push("/account/support/new"),
}}
/>
diff --git a/packages/domain/auth/forms.ts b/packages/domain/auth/forms.ts
index 053a2dfb..55bf2ee7 100644
--- a/packages/domain/auth/forms.ts
+++ b/packages/domain/auth/forms.ts
@@ -64,7 +64,7 @@ export function getPasswordStrengthDisplay(strength: number): {
export const MIGRATION_TRANSFER_ITEMS = [
"All active services",
"Billing history",
- "Support tickets",
+ "Support cases",
"Account details",
] as const;
diff --git a/packages/domain/auth/schema.ts b/packages/domain/auth/schema.ts
index b5a4d50a..9c31f90a 100644
--- a/packages/domain/auth/schema.ts
+++ b/packages/domain/auth/schema.ts
@@ -41,7 +41,7 @@ export const signupInputSchema = z.object({
lastName: nameSchema,
company: z.string().optional(),
phone: phoneSchema,
- sfNumber: z.string().min(6, "Customer number must be at least 6 characters"),
+ sfNumber: z.string().trim().min(6, "Customer number must be at least 6 characters").optional(),
address: addressSchema.optional(),
nationality: z.string().optional(),
dateOfBirth: isoDateOnlySchema.optional(),
@@ -85,7 +85,7 @@ export const linkWhmcsRequestSchema = z.object({
});
export const validateSignupRequestSchema = z.object({
- sfNumber: z.string().min(1, "Customer number is required"),
+ sfNumber: z.string().trim().min(1, "Customer number is required").optional(),
});
/**