Refactor Internet Components and Enhance User Experience

- Updated InternetImportantNotes component title for clarity.
- Refined InternetOfferingCard by removing unused props and simplifying the layout for better usability.
- Enhanced PlanComparisonGuide with collapsible sections for improved navigation and readability.
- Streamlined PublicOfferingCard to support custom call-to-action labels and click handlers.
- Improved SimTypeSelector with additional EID information and verification notices for better user guidance.
- Updated PublicInternetPlans and PublicSimPlans views to incorporate new service highlights and improve overall presentation.
This commit is contained in:
barsa 2025-12-24 19:01:21 +09:00
parent 47414f10e0
commit 3bb4e8ce40
81 changed files with 3318 additions and 2955 deletions

View File

@ -201,7 +201,7 @@ export class MeStatusService {
description:
"Were verifying if our service is available at your residence. Well notify you when review is complete.",
actionLabel: "View status",
detailHref: "/account/shop/internet",
detailHref: "/account/services/internet",
tone: "info",
});
}
@ -229,7 +229,7 @@ export class MeStatusService {
title: "Start your first service",
description: "Browse our catalog and subscribe to internet, SIM, or VPN",
actionLabel: "Browse services",
detailHref: "/shop",
detailHref: "/services",
tone: "neutral",
});
}

View File

@ -316,7 +316,7 @@ export class OrderValidator {
const eligibility = await this.internetCatalogService.getEligibilityDetailsForUser(userId);
if (eligibility.status === "not_requested") {
throw new BadRequestException(
"Internet eligibility review is required before ordering. Please request an eligibility review from the Internet shop page and try again."
"Internet eligibility review is required before ordering. Please request an eligibility review from the Internet services page and try again."
);
}
if (eligibility.status === "pending") {

View File

@ -1,11 +0,0 @@
/**
* Public Catalog Layout
*
* Shop pages with catalog navigation and auth-aware header.
*/
import { CatalogShell } from "@/components/templates/CatalogShell";
export default function PublicCatalogLayout({ children }: { children: React.ReactNode }) {
return <CatalogShell>{children}</CatalogShell>;
}

View File

@ -1,17 +0,0 @@
/**
* Public Internet Configure Page
*
* Configure internet plan for unauthenticated users.
*/
import { PublicInternetConfigureView } from "@/features/catalog/views/PublicInternetConfigure";
import { RedirectAuthenticatedToAccountShop } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountShop";
export default function PublicInternetConfigurePage() {
return (
<>
<RedirectAuthenticatedToAccountShop targetPath="/account/shop/internet/configure" />
<PublicInternetConfigureView />
</>
);
}

View File

@ -1,17 +0,0 @@
/**
* Public Internet Plans Page
*
* Displays internet plans for unauthenticated users.
*/
import { PublicInternetPlansView } from "@/features/catalog/views/PublicInternetPlans";
import { RedirectAuthenticatedToAccountShop } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountShop";
export default function PublicInternetPlansPage() {
return (
<>
<RedirectAuthenticatedToAccountShop targetPath="/account/shop/internet" />
<PublicInternetPlansView />
</>
);
}

View File

@ -1,9 +0,0 @@
/**
* Public Shop Layout
*
* CatalogShell is applied at `(public)/(catalog)/layout.tsx`.
*/
export default function CatalogLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@ -1,28 +0,0 @@
import { Skeleton } from "@/components/atoms/loading-skeleton";
export default function CatalogLoading() {
return (
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<Skeleton className="h-8 w-48 mb-4" />
<Skeleton className="h-10 w-96 mb-2" />
<Skeleton className="h-6 w-[32rem] max-w-full" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-10">
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className="bg-card rounded-xl border border-border p-6 space-y-3 shadow-[var(--cp-shadow-1)]"
>
<Skeleton className="h-12 w-12 rounded-xl" />
<Skeleton className="h-6 w-1/2" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-10 w-full mt-4" />
</div>
))}
</div>
</div>
);
}

View File

@ -1,17 +0,0 @@
/**
* Public Catalog Home Page
*
* Displays the catalog home with service cards for Internet, SIM, and VPN.
*/
import { PublicCatalogHomeView } from "@/features/catalog/views/PublicCatalogHome";
import { RedirectAuthenticatedToAccountShop } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountShop";
export default function PublicCatalogPage() {
return (
<>
<RedirectAuthenticatedToAccountShop targetPath="/account/shop" />
<PublicCatalogHomeView />
</>
);
}

View File

@ -1,17 +0,0 @@
/**
* Public SIM Configure Page
*
* Configure SIM plan for unauthenticated users.
*/
import { PublicSimConfigureView } from "@/features/catalog/views/PublicSimConfigure";
import { RedirectAuthenticatedToAccountShop } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountShop";
export default function PublicSimConfigurePage() {
return (
<>
<RedirectAuthenticatedToAccountShop targetPath="/account/shop/sim/configure" />
<PublicSimConfigureView />
</>
);
}

View File

@ -1,17 +0,0 @@
/**
* Public SIM Plans Page
*
* Displays SIM plans for unauthenticated users.
*/
import { PublicSimPlansView } from "@/features/catalog/views/PublicSimPlans";
import { RedirectAuthenticatedToAccountShop } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountShop";
export default function PublicSimPlansPage() {
return (
<>
<RedirectAuthenticatedToAccountShop targetPath="/account/shop/sim" />
<PublicSimPlansView />
</>
);
}

View File

@ -1,17 +0,0 @@
/**
* Public VPN Plans Page
*
* Displays VPN plans for unauthenticated users.
*/
import { PublicVpnPlansView } from "@/features/catalog/views/PublicVpnPlans";
import { RedirectAuthenticatedToAccountShop } from "@/features/catalog/components/common/RedirectAuthenticatedToAccountShop";
export default function PublicVpnPlansPage() {
return (
<>
<RedirectAuthenticatedToAccountShop targetPath="/account/shop/vpn" />
<PublicVpnPlansView />
</>
);
}

View File

@ -1,5 +0,0 @@
import InternetCancelContainer from "@/features/subscriptions/views/InternetCancel";
export default function AccountInternetCancelPage() {
return <InternetCancelContainer />;
}

View File

@ -1,19 +0,0 @@
import { RouteLoading } from "@/components/molecules/RouteLoading";
import { ServerIcon } from "@heroicons/react/24/outline";
import { LoadingCard } from "@/components/atoms/loading-skeleton";
export default function AccountServiceDetailLoading() {
return (
<RouteLoading
icon={<ServerIcon />}
title="Service"
description="Service details"
mode="content"
>
<div className="space-y-4">
<LoadingCard />
<LoadingCard />
</div>
</RouteLoading>
);
}

View File

@ -1,5 +0,0 @@
import SubscriptionDetailContainer from "@/features/subscriptions/views/SubscriptionDetail";
export default function AccountServiceDetailPage() {
return <SubscriptionDetailContainer />;
}

View File

@ -1,5 +0,0 @@
import SimCallHistoryContainer from "@/features/subscriptions/views/SimCallHistory";
export default function AccountSimCallHistoryPage() {
return <SimCallHistoryContainer />;
}

View File

@ -1,5 +0,0 @@
import SimCancelContainer from "@/features/subscriptions/views/SimCancel";
export default function AccountSimCancelPage() {
return <SimCancelContainer />;
}

View File

@ -1,5 +0,0 @@
import SimChangePlanContainer from "@/features/subscriptions/views/SimChangePlan";
export default function AccountSimChangePlanPage() {
return <SimChangePlanContainer />;
}

View File

@ -1,5 +0,0 @@
import SimReissueContainer from "@/features/subscriptions/views/SimReissue";
export default function AccountSimReissuePage() {
return <SimReissueContainer />;
}

View File

@ -1,5 +0,0 @@
import SimTopUpContainer from "@/features/subscriptions/views/SimTopUp";
export default function AccountSimTopUpPage() {
return <SimTopUpContainer />;
}

View File

@ -1,16 +0,0 @@
import { RouteLoading } from "@/components/molecules/RouteLoading";
import { ServerIcon } from "@heroicons/react/24/outline";
import { LoadingTable } from "@/components/atoms/loading-skeleton";
export default function AccountServicesLoading() {
return (
<RouteLoading
icon={<ServerIcon />}
title="Services"
description="View and manage your services"
mode="content"
>
<LoadingTable rows={6} columns={5} />
</RouteLoading>
);
}

View File

@ -1,5 +1,5 @@
import SubscriptionsListContainer from "@/features/subscriptions/views/SubscriptionsList";
import { CatalogHomeView } from "@/features/catalog/views/CatalogHome";
export default function AccountServicesPage() {
return <SubscriptionsListContainer />;
export default function AccountShopPage() {
return <CatalogHomeView />;
}

View File

@ -1,5 +0,0 @@
import { InternetConfigureContainer } from "@/features/catalog/views/InternetConfigure";
export default function AccountInternetConfigurePage() {
return <InternetConfigureContainer />;
}

View File

@ -1,5 +0,0 @@
import { InternetPlansContainer } from "@/features/catalog/views/InternetPlans";
export default function AccountInternetPlansPage() {
return <InternetPlansContainer />;
}

View File

@ -1,5 +0,0 @@
import type { ReactNode } from "react";
export default function AccountShopLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}

View File

@ -1,5 +0,0 @@
import { CatalogHomeView } from "@/features/catalog/views/CatalogHome";
export default function AccountShopPage() {
return <CatalogHomeView />;
}

View File

@ -1,5 +0,0 @@
import { SimConfigureContainer } from "@/features/catalog/views/SimConfigure";
export default function AccountSimConfigurePage() {
return <SimConfigureContainer />;
}

View File

@ -1,5 +0,0 @@
import { SimPlansContainer } from "@/features/catalog/views/SimPlans";
export default function AccountSimPlansPage() {
return <SimPlansContainer />;
}

View File

@ -1,5 +0,0 @@
import { VpnPlansView } from "@/features/catalog/views/VpnPlans";
export default function AccountVpnPlansPage() {
return <VpnPlansView />;
}

View File

@ -39,9 +39,9 @@ export const baseNavigation: NavigationItem[] = [
{
name: "My Services",
icon: ServerIcon,
children: [{ name: "All Services", href: "/account/services" }],
children: [{ name: "All Services", href: "/account/my-services" }],
},
{ name: "Shop", href: "/account/shop", icon: Squares2X2Icon },
{ name: "Services", href: "/account/services", icon: Squares2X2Icon },
{
name: "Support",
icon: ChatBubbleLeftRightIcon,
@ -68,13 +68,13 @@ export function computeNavigation(activeSubscriptions?: Subscription[]): Navigat
if (subIdx >= 0) {
const dynamicChildren = (activeSubscriptions || []).map(sub => ({
name: truncate(sub.productName || `Subscription ${sub.id}`, 28),
href: `/account/services/${sub.id}`,
href: `/account/my-services/${sub.id}`,
tooltip: sub.productName || `Subscription ${sub.id}`,
}));
nav[subIdx] = {
...nav[subIdx],
children: [{ name: "All Services", href: "/account/services" }, ...dynamicChildren],
children: [{ name: "All Services", href: "/account/my-services" }, ...dynamicChildren],
};
}

View File

@ -5,3 +5,4 @@
export { AppShell } from "./AppShell/AppShell";
export { AgentforceWidget } from "./AgentforceWidget";
export { SiteFooter } from "./SiteFooter";

View File

@ -42,19 +42,14 @@ export function AuthLayout({
)}
<div className="text-center">
<div className="flex justify-center mb-6">
<div className="relative">
<div className="absolute inset-0 bg-[#28A6E0]/15 blur-2xl rounded-full scale-[2]" />
<div className="relative h-[4.5rem] w-[4.5rem] rounded-2xl bg-white border border-border/50 flex items-center justify-center shadow-xl shadow-[#28A6E0]/15">
<Logo size={44} />
</div>
<div className="flex justify-center mb-8">
<div className="h-16 w-16 rounded-xl bg-primary/5 flex items-center justify-center">
<Logo size={40} />
</div>
</div>
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-foreground mb-2">
{title}
</h1>
<h1 className="text-2xl font-bold tracking-tight text-foreground mb-2">{title}</h1>
{subtitle && (
<p className="text-sm sm:text-base text-muted-foreground leading-relaxed max-w-sm mx-auto">
<p className="text-sm text-muted-foreground leading-relaxed max-w-sm mx-auto">
{subtitle}
</p>
)}
@ -62,18 +57,14 @@ export function AuthLayout({
</div>
<div className={`mt-8 w-full ${maxWidth}`}>
<div className="relative">
{/* Subtle gradient glow behind card */}
<div className="absolute -inset-1 bg-gradient-to-r from-primary/10 via-transparent to-primary/10 rounded-[1.75rem] blur-xl opacity-50" />
<div className="relative bg-card text-card-foreground py-10 px-6 rounded-2xl border border-border/60 shadow-xl shadow-black/5 sm:px-10 backdrop-blur-sm">
{children}
</div>
<div className="bg-card text-card-foreground py-8 px-6 rounded-xl border border-border shadow-sm sm:px-10">
{children}
</div>
{/* Trust indicator */}
<div className="mt-6 text-center">
<p className="text-xs text-muted-foreground/60">
<div className="mt-8 text-center">
<p className="text-xs text-muted-foreground/60 flex items-center justify-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-green-500/80" />
Secure login protected by SSL encryption
</p>
</div>

View File

@ -1,127 +0,0 @@
/**
* CatalogShell - Public catalog layout shell
*
* Used for public catalog pages with catalog-specific navigation.
* 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";
import { ShopTabs } from "@/features/catalog/components/base/ShopTabs";
export interface CatalogShellProps {
children: ReactNode;
}
export function CatalogNav() {
return <ShopTabs basePath="/shop" />;
}
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 (
<div className="min-h-screen flex flex-col bg-background text-foreground">
{/* Subtle background pattern */}
<div className="fixed inset-0 -z-10 overflow-hidden pointer-events-none">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
</div>
<header className="sticky top-0 z-40 border-b border-border/50 bg-background/80 backdrop-blur-xl">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-3 flex items-center justify-between gap-4">
{/* Logo */}
<Link href="/" className="inline-flex items-center gap-3 min-w-0 group">
<span className="inline-flex items-center justify-center h-11 w-11 rounded-xl bg-white border border-border/60 shadow-lg shadow-[#28A6E0]/10 transition-transform group-hover:scale-105">
<Logo size={28} />
</span>
<span className="min-w-0">
<span className="block text-base font-bold leading-tight truncate text-foreground">
Assist Solutions
</span>
<span className="block text-xs text-muted-foreground leading-tight truncate">
Account Portal
</span>
</span>
</Link>
{/* Right side actions */}
<div className="flex items-center gap-2">
<Link
href="/help"
className="hidden sm:inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
>
Support
</Link>
{isAuthenticated ? (
<Link
href="/account"
className="inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary-hover shadow-sm shadow-primary/20 transition-all hover:shadow-md hover:shadow-primary/30"
>
My Account
</Link>
) : (
<Link
href="/auth/login"
className="inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary-hover shadow-sm shadow-primary/20 transition-all hover:shadow-md hover:shadow-primary/30"
>
Sign in
</Link>
)}
</div>
</div>
</header>
<CatalogNav />
<main className="flex-1">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-8 sm:py-12">
{children}
</div>
</main>
<footer className="border-t border-border/50 bg-muted/30">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-8">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="text-sm text-muted-foreground">
© {new Date().getFullYear()} Assist Solutions. All rights reserved.
</div>
<div className="flex items-center gap-6 text-sm">
<Link
href="/help"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Support
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Privacy
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Terms
</Link>
</div>
</div>
</div>
</footer>
</div>
);
}

View File

@ -1,2 +0,0 @@
export { CatalogNav, CatalogShell } from "./CatalogShell";
export type { CatalogShellProps } from "./CatalogShell";

View File

@ -10,6 +10,7 @@ import type { ReactNode } from "react";
import { useEffect } from "react";
import Link from "next/link";
import { Logo } from "@/components/atoms/logo";
import { SiteFooter } from "@/components/organisms/SiteFooter";
import { useAuthStore } from "@/features/auth/services/auth.store";
export interface PublicShellProps {
@ -29,52 +30,58 @@ export function PublicShell({ children }: PublicShellProps) {
return (
<div className="min-h-screen flex flex-col bg-background text-foreground">
{/* Subtle background pattern */}
<div className="fixed inset-0 -z-10 overflow-hidden pointer-events-none">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
</div>
{/* Subtle background pattern - clean and minimal */}
<div className="fixed inset-0 -z-10 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/5 via-background to-background" />
<header className="sticky top-0 z-40 border-b border-border/50 bg-background/80 backdrop-blur-xl">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-3 flex items-center justify-between gap-4">
<Link href="/" className="inline-flex items-center gap-3 min-w-0 group">
<span className="inline-flex items-center justify-center h-11 w-11 rounded-xl bg-white border border-border/60 shadow-lg shadow-[#28A6E0]/10 transition-transform group-hover:scale-105">
<Logo size={28} />
<header className="sticky top-0 z-40 border-b border-border/40 bg-background/95 backdrop-blur-md supports-[backdrop-filter]:bg-background/80">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] h-16 flex items-center justify-between gap-4">
<Link href="/" className="inline-flex items-center gap-2.5 min-w-0 group">
<span className="inline-flex items-center justify-center h-9 w-9 rounded-lg bg-primary/10 text-primary transition-colors group-hover:bg-primary/15">
<Logo size={20} />
</span>
<span className="min-w-0">
<span className="block text-base font-bold leading-tight truncate text-foreground">
<span className="min-w-0 hidden sm:block">
<span className="block text-base font-bold leading-none tracking-tight text-foreground">
Assist Solutions
</span>
<span className="block text-xs text-muted-foreground leading-tight truncate">
Account Portal
</span>
</span>
</Link>
<nav className="flex items-center gap-2">
<nav className="flex items-center gap-1 sm:gap-2">
<Link
href="/shop"
className="hidden sm:inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
href="/services"
className="inline-flex items-center rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
Services
</Link>
<Link
href="/about"
className="hidden sm:inline-flex items-center rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
About
</Link>
<Link
href="/contact"
className="hidden sm:inline-flex items-center rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
Contact
</Link>
<Link
href="/help"
className="hidden sm:inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
className="hidden md:inline-flex items-center rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
Support
</Link>
{isAuthenticated ? (
<Link
href="/account"
className="inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary-hover shadow-sm shadow-primary/20 transition-all hover:shadow-md hover:shadow-primary/30"
className="ml-2 inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
My Account
</Link>
) : (
<Link
href="/auth/login"
className="inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary-hover shadow-sm shadow-primary/20 transition-all hover:shadow-md hover:shadow-primary/30"
className="ml-2 inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
Sign in
</Link>
@ -89,41 +96,7 @@ export function PublicShell({ children }: PublicShellProps) {
</div>
</main>
<footer className="border-t border-border/50 bg-muted/30">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-8">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="text-sm text-muted-foreground">
© {new Date().getFullYear()} Assist Solutions. All rights reserved.
</div>
<div className="flex items-center gap-6 text-sm">
<Link
href="/shop"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Services
</Link>
<Link
href="/help"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Support
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Privacy
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Terms
</Link>
</div>
</div>
</div>
</footer>
<SiteFooter />
</div>
);
}

View File

@ -6,9 +6,6 @@
export { AuthLayout } from "./AuthLayout/AuthLayout";
export type { AuthLayoutProps } from "./AuthLayout/AuthLayout";
export { CatalogShell } from "./CatalogShell/CatalogShell";
export type { CatalogShellProps } from "./CatalogShell/CatalogShell";
export { PageLayout } from "./PageLayout/PageLayout";
export type { BreadcrumbItem } from "./PageLayout/PageLayout";

View File

@ -0,0 +1,50 @@
import { CheckCircleIcon } from "@heroicons/react/24/solid";
export interface HighlightFeature {
icon: React.ReactNode;
title: string;
description: string;
highlight?: string;
}
interface ServiceHighlightsProps {
features: HighlightFeature[];
className?: string;
}
function HighlightItem({ icon, title, description, highlight }: HighlightFeature) {
return (
<div className="flex flex-col h-full p-5 rounded-2xl bg-card border border-border shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-start justify-between gap-4 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary flex-shrink-0">
{icon}
</div>
{highlight && (
<span className="inline-flex items-center gap-1.5 py-1 px-2.5 rounded-full bg-success/10 text-[10px] font-bold text-success leading-tight text-right max-w-[65%]">
<CheckCircleIcon className="h-3.5 w-3.5 flex-shrink-0" />
<span className="break-words">{highlight}</span>
</span>
)}
</div>
<h3 className="font-bold text-foreground text-base mb-2">{title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed flex-grow">{description}</p>
</div>
);
}
/**
* ServiceHighlights
*
* A clean, grid-based layout for displaying service features/highlights.
* Replaces the old boxed "Why Choose Us" sections.
*/
export function ServiceHighlights({ features, className = "" }: ServiceHighlightsProps) {
return (
<div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 ${className}`}>
{features.map((feature, index) => (
<HighlightItem key={index} {...feature} />
))}
</div>
);
}

View File

@ -1,48 +0,0 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
type BasePath = "/shop" | "/account/shop";
type Tab = {
label: string;
href: `${BasePath}` | `${BasePath}/${string}`;
};
export function ShopTabs({ basePath }: { basePath: BasePath }) {
const pathname = usePathname();
const tabs: Tab[] = [
{ label: "All Services", href: basePath },
{ label: "Internet", href: `${basePath}/internet` },
{ label: "SIM", href: `${basePath}/sim` },
{ label: "VPN", href: `${basePath}/vpn` },
];
const isActive = (href: string) => pathname === href || pathname.startsWith(`${href}/`);
return (
<div className="border-b border-border/50 bg-background/60 backdrop-blur-xl">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-2">
<nav className="flex items-center gap-1 overflow-x-auto" aria-label="Shop sections">
{tabs.map(tab => (
<Link
key={tab.href}
href={tab.href}
className={cn(
"whitespace-nowrap inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors",
isActive(tab.href)
? "bg-muted text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
)}
>
{tab.label}
</Link>
))}
</nav>
</div>
</div>
);
}

View File

@ -1,37 +0,0 @@
"use client";
import { useEffect } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useAuthStore } from "@/features/auth/services/auth.store";
type Props = {
/**
* Absolute target path (no querystring). When omitted, the current pathname is transformed:
* `/shop/...` -> `/account/shop/...`.
*/
targetPath?: string;
};
export function RedirectAuthenticatedToAccountShop({ targetPath }: Props) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const isAuthenticated = useAuthStore(state => state.isAuthenticated);
const hasCheckedAuth = useAuthStore(state => state.hasCheckedAuth);
useEffect(() => {
if (!hasCheckedAuth) return;
if (!isAuthenticated) return;
const nextPath =
targetPath ??
(pathname.startsWith("/shop")
? pathname.replace(/^\/shop/, "/account/shop")
: "/account/shop");
const query = searchParams?.toString() ?? "";
router.replace(query ? `${nextPath}?${query}` : nextPath);
}, [hasCheckedAuth, isAuthenticated, pathname, router, searchParams, targetPath]);
return null;
}

View File

@ -41,3 +41,8 @@ export type {
export type { ConfigurationStepProps, StepValidation } from "./base/ConfigurationStep";
export type { AddressFormProps } from "./base/AddressForm";
export type { PaymentFormProps } from "./base/PaymentForm";
// Common components
export { RedirectAuthenticatedToAccountServices } from "./common/RedirectAuthenticatedToAccountServices";
export { FeatureCard } from "./common/FeatureCard";
export { ServiceHeroCard } from "./common/ServiceHeroCard";

View File

@ -8,7 +8,7 @@ export function InternetImportantNotes() {
<div className="flex items-start gap-3 mb-4">
<InformationCircleIcon className="h-5 w-5 text-info flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-foreground">Before you choose a plan</h3>
<h3 className="font-semibold text-foreground">Important notes & fees</h3>
<p className="text-sm text-muted-foreground mt-1">
A few things to keep in mind when selecting your internet service.
</p>

View File

@ -1,17 +1,9 @@
"use client";
import { useState } from "react";
import {
ChevronDownIcon,
ChevronUpIcon,
HomeIcon,
BuildingOfficeIcon,
BoltIcon,
} from "@heroicons/react/24/outline";
import { HomeIcon, BuildingOfficeIcon, BoltIcon } from "@heroicons/react/24/outline";
import { Button } from "@/components/atoms/button";
import { CardBadge } from "@/features/catalog/components/base/CardBadge";
import { cn } from "@/lib/utils";
import { InternetTierPricingModal } from "@/features/catalog/components/internet/InternetTierPricingModal";
interface TierInfo {
tier: "Silver" | "Gold" | "Platinum";
@ -19,38 +11,24 @@ interface TierInfo {
description: string;
features: string[];
recommended?: boolean;
/** Additional pricing note (e.g., for Platinum's equipment fees) */
pricingNote?: string;
}
interface InternetOfferingCardProps {
/** Offering type identifier */
offeringType: string;
/** Display title */
title: string;
/** Speed badge text */
speedBadge: string;
/** Short description */
description: string;
/** Icon type */
iconType: "home" | "apartment";
/** Starting monthly price */
startingPrice: number;
/** Setup fee */
setupFee: number;
/** Tier options */
tiers: TierInfo[];
/** Whether this is a premium/select-area option */
isPremium?: boolean;
/** CTA path */
ctaPath: string;
/** Whether to expand by default */
// defaultExpanded is no longer used but kept for prop compatibility if needed upstream
defaultExpanded?: boolean;
/** Whether the card is disabled (e.g., already subscribed) */
disabled?: boolean;
/** Reason for being disabled */
disabledReason?: string;
/** Preview mode - hides action buttons, shows informational text instead */
previewMode?: boolean;
}
@ -79,201 +57,142 @@ export function InternetOfferingCard({
tiers,
isPremium = false,
ctaPath,
defaultExpanded = false,
disabled = false,
disabledReason,
previewMode = false,
}: InternetOfferingCardProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [pricingOpen, setPricingOpen] = useState(false);
const Icon = iconType === "home" ? HomeIcon : BuildingOfficeIcon;
return (
<div
className={cn(
"rounded-2xl border bg-card shadow-[var(--cp-shadow-1)] overflow-hidden transition-all duration-300",
isExpanded ? "shadow-[var(--cp-shadow-2)]" : "",
"rounded-xl border bg-card shadow-[var(--cp-shadow-1)] overflow-hidden",
isPremium ? "border-primary/30" : "border-border"
)}
>
{/* Header - Always visible */}
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full p-5 sm:p-6 flex items-start justify-between gap-4 text-left hover:bg-muted/30 transition-colors"
>
<div className="flex items-start gap-4">
<div className="w-full p-4 flex items-start justify-between gap-3 text-left">
<div className="flex items-start gap-3">
<div
className={cn(
"flex h-12 w-12 items-center justify-center rounded-xl border flex-shrink-0",
"flex h-10 w-10 items-center justify-center rounded-lg border flex-shrink-0",
iconType === "home"
? "bg-info-soft/50 text-info border-info/20"
: "bg-success-soft/50 text-success border-success/20"
)}
>
<Icon className="h-6 w-6" />
<Icon className="h-5 w-5" />
</div>
<div className="space-y-1.5">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-lg font-bold text-foreground">{title}</h3>
<h3 className="text-base font-bold text-foreground">{title}</h3>
<CardBadge text={speedBadge} variant={isPremium ? "new" : "default"} size="sm" />
{isPremium && <span className="text-xs text-muted-foreground">(select areas)</span>}
</div>
<p className="text-sm text-muted-foreground">{description}</p>
<div className="flex items-baseline gap-1 pt-1">
<div className="flex items-baseline gap-1 pt-0.5">
<span className="text-xs text-muted-foreground">From</span>
<span className="text-xl font-bold text-foreground">
<span className="text-lg font-bold text-foreground">
¥{startingPrice.toLocaleString()}
</span>
<span className="text-sm text-muted-foreground">/month</span>
<span className="text-xs text-muted-foreground ml-2">
<span className="text-sm text-muted-foreground">/mo</span>
<span className="text-xs text-muted-foreground ml-1.5">
+ ¥{setupFee.toLocaleString()} setup
</span>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0 mt-2">
<span className="text-sm text-muted-foreground hidden sm:inline">
{previewMode
? isExpanded
? "Hide tiers"
: "Preview tiers"
: isExpanded
? "Hide plans"
: "View plans"}
</span>
{isExpanded ? (
<ChevronUpIcon className="h-5 w-5 text-muted-foreground" />
) : (
<ChevronDownIcon className="h-5 w-5 text-muted-foreground" />
)}
</div>
</button>
{/* Expanded content - Tier options */}
{isExpanded && (
<div className="border-t border-border px-5 sm:px-6 py-5 bg-muted/20">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{tiers.map(tier => (
<div
key={tier.tier}
className={cn(
"rounded-xl border p-4 transition-all duration-200 hover:shadow-md flex flex-col",
tierStyles[tier.tier].card,
tier.recommended && "ring-2 ring-warning/30"
{/* Tiers - Always expanded */}
<div className="border-t border-border px-4 py-4 bg-muted/10">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{tiers.map(tier => (
<div
key={tier.tier}
className={cn(
"rounded-lg border p-3 transition-all flex flex-col",
tierStyles[tier.tier].card,
tier.recommended && "ring-1 ring-warning/30"
)}
>
{/* Header */}
<div className="flex items-center gap-2 mb-2">
<span className={cn("font-semibold text-sm", tierStyles[tier.tier].accent)}>
{tier.tier}
</span>
{tier.recommended && (
<CardBadge text="Recommended" variant="recommended" size="xs" />
)}
>
{/* Header */}
<div className="flex items-center gap-2 mb-3">
<span className={cn("font-bold", tierStyles[tier.tier].accent)}>{tier.tier}</span>
{tier.recommended ? (
<CardBadge text="Recommended" variant="recommended" size="xs" />
) : null}
</div>
{/* Price */}
{!previewMode && (
<div className="mb-2">
<div className="flex items-baseline gap-0.5 flex-wrap">
<span className="text-xl font-bold text-foreground">
¥{tier.monthlyPrice.toLocaleString()}
</span>
<span className="text-xs text-muted-foreground">/mo</span>
{tier.pricingNote && (
<span className="text-[10px] text-warning ml-1">{tier.pricingNote}</span>
)}
</div>
</div>
)}
{/* Pricing (hidden in preview mode) */}
{!previewMode ? (
<div className="mb-3">
<div className="flex items-baseline gap-1">
<span className="text-2xl font-bold text-foreground">
¥{tier.monthlyPrice.toLocaleString()}
</span>
<span className="text-sm text-muted-foreground">/mo</span>
</div>
{tier.pricingNote ? (
<p className="text-xs text-warning mt-1">{tier.pricingNote}</p>
) : null}
</div>
) : null}
{/* Description */}
<p className="text-xs text-muted-foreground mb-2">{tier.description}</p>
{/* Description */}
<p className="text-sm text-muted-foreground mb-3">{tier.description}</p>
{/* Features */}
<ul className="space-y-1 flex-grow mb-3">
{tier.features.slice(0, 3).map((feature, index) => (
<li key={index} className="flex items-start gap-1.5 text-xs">
<BoltIcon className="h-3 w-3 text-success flex-shrink-0 mt-0.5" />
<span className="text-muted-foreground leading-relaxed">{feature}</span>
</li>
))}
</ul>
{/* Features */}
<ul className={cn("space-y-1.5 mb-4 flex-grow", previewMode ? "opacity-90" : "")}>
{(previewMode ? tier.features.slice(0, 3) : tier.features).map(
(feature, index) => (
<li key={index} className="flex items-start gap-2 text-sm">
<BoltIcon className="h-3.5 w-3.5 text-success flex-shrink-0 mt-0.5" />
<span className="text-muted-foreground text-xs leading-relaxed">
{feature}
</span>
</li>
)
)}
</ul>
{/* Button/Info - always at bottom */}
{previewMode ? (
<div className="mt-auto pt-2 border-t border-border/50">
<p className="text-xs text-muted-foreground text-center">
Prices shown after you click See pricing
</p>
</div>
) : disabled ? (
<div className="mt-auto">
<Button variant="outline" size="sm" className="w-full" disabled>
Unavailable
</Button>
{disabledReason ? (
<p className="text-xs text-muted-foreground text-center mt-2">
{disabledReason}
</p>
) : null}
</div>
) : (
<Button
as="a"
href={ctaPath}
variant={tier.recommended ? "default" : "outline"}
size="sm"
className="w-full mt-auto"
>
Get Started
{/* Button */}
{previewMode ? (
<div className="mt-auto pt-2 border-t border-border/50">
<p className="text-[10px] text-muted-foreground text-center">
See pricing after verification
</p>
</div>
) : disabled ? (
<div className="mt-auto">
<Button variant="outline" size="sm" className="w-full" disabled>
Unavailable
</Button>
)}
</div>
))}
</div>
{previewMode ? (
<div className="mt-5 flex flex-col sm:flex-row items-stretch sm:items-center gap-3 sm:justify-between">
<p className="text-xs text-muted-foreground">
Setup is typically ¥{setupFee.toLocaleString()}. Your actual options are confirmed
after address verification.
</p>
<div className="flex flex-col sm:flex-row gap-2 sm:flex-shrink-0">
<Button type="button" variant="outline" onClick={() => setPricingOpen(true)}>
See pricing
{disabledReason && (
<p className="text-[10px] text-muted-foreground text-center mt-1.5">
{disabledReason}
</p>
)}
</div>
) : (
<Button
as="a"
href={ctaPath}
variant={tier.recommended ? "default" : "outline"}
size="sm"
className="w-full mt-auto"
>
Select
</Button>
<Button as="a" href={ctaPath}>
Check availability
</Button>
</div>
)}
</div>
) : (
<p className="text-xs text-muted-foreground text-center mt-4">
+ ¥{setupFee.toLocaleString()} one-time installation (or 12/24-month installment)
</p>
)}
))}
</div>
)}
{/* Pricing modal (public preview mode only) */}
{previewMode ? (
<InternetTierPricingModal
isOpen={pricingOpen}
onClose={() => setPricingOpen(false)}
offeringTitle={title}
offeringSubtitle={`${speedBadge}${isPremium ? " · select areas" : ""}`}
tiers={tiers}
setupFee={setupFee}
ctaHref={ctaPath}
/>
) : null}
{/* Footer */}
<p className="text-xs text-muted-foreground text-center mt-3 pt-3 border-t border-border/50">
+ ¥{setupFee.toLocaleString()} one-time installation (or 12/24-month installment)
</p>
</div>
</div>
);
}

View File

@ -14,14 +14,14 @@ import type { BadgeVariant } from "@/features/catalog/components/base/CardBadge"
import { useCatalogStore } from "@/features/catalog/services/catalog.store";
import { IS_DEVELOPMENT } from "@/config/environment";
import { parsePlanName } from "@/features/catalog/components/internet/utils/planName";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
interface InternetPlanCardProps {
plan: InternetPlanCatalogItem;
installations: InternetInstallationCatalogItem[];
disabled?: boolean;
disabledReason?: string;
/** Override the default configure href (default: /shop/internet/configure?planSku=...) */
/** Override the default configure href (default: /services/internet/configure?planSku=...) */
configureHref?: string;
/** Override default "Configure Plan" action (used for public browse-only flows) */
action?: { label: string; href: string };
@ -71,7 +71,7 @@ export function InternetPlanCard({
titlePriority = "detail",
}: InternetPlanCardProps) {
const router = useRouter();
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const tier = plan.internetPlanTier;
const isGold = tier === "Gold";
const isPlatinum = tier === "Platinum";
@ -249,7 +249,7 @@ export function InternetPlanCard({
setInternetConfig({ planSku: plan.sku, currentStep: 1 });
const href =
configureHref ??
`${shopBasePath}/internet/configure?planSku=${encodeURIComponent(plan.sku)}`;
`${servicesBasePath}/internet/configure?planSku=${encodeURIComponent(plan.sku)}`;
router.push(href);
}}
>

View File

@ -1,9 +1,13 @@
"use client";
import { useState } from "react";
import {
WrenchScrewdriverIcon,
SparklesIcon,
CubeTransparentIcon,
ChevronDownIcon,
ChevronUpIcon,
QuestionMarkCircleIcon,
} from "@heroicons/react/24/outline";
import { cn } from "@/lib/utils";
@ -56,7 +60,7 @@ function PlanGuideItem({
highlight && "ring-2 ring-warning/30"
)}
>
<div className="flex items-start gap-4">
<div className="flex items-start gap-3">
<div
className={cn(
"flex h-10 w-10 items-center justify-center rounded-lg border flex-shrink-0",
@ -83,52 +87,78 @@ function PlanGuideItem({
}
export function PlanComparisonGuide() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<section className="bg-card rounded-2xl border border-border shadow-[var(--cp-shadow-1)] p-5 sm:p-6">
<div className="mb-5">
<h3 className="text-lg font-bold text-foreground mb-1">Which plan is right for you?</h3>
<p className="text-sm text-muted-foreground">
All plans include the same connection speed. The difference is in equipment and support.
</p>
</div>
<section className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
{/* Collapsible header */}
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full px-4 py-3.5 flex items-center justify-between gap-3 text-left hover:bg-muted/30 transition-colors"
>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-info-soft/50 text-info border border-info/20 flex-shrink-0">
<QuestionMarkCircleIcon className="h-4 w-4" />
</div>
<div>
<h3 className="text-sm font-bold text-foreground">Which tier is right for you?</h3>
<p className="text-xs text-muted-foreground">
Same speeds across all tiersdifference is equipment & support
</p>
</div>
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<span className="text-xs text-muted-foreground hidden sm:inline">
{isExpanded ? "Hide" : "Compare tiers"}
</span>
{isExpanded ? (
<ChevronUpIcon className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronDownIcon className="h-4 w-4 text-muted-foreground" />
)}
</div>
</button>
{/* Stacked rows - always vertical for cleaner reading */}
<div className="space-y-3">
<PlanGuideItem
tier="Silver"
icon={<WrenchScrewdriverIcon className="h-5 w-5" />}
title="Silver"
idealFor="Tech-savvy users with their own router"
description="You get the NTT modem and ISP connection. Bring your own WiFi router and configure the network yourself. Best for those comfortable with networking."
/>
{/* Expandable content */}
{isExpanded && (
<div className="px-4 pb-4 border-t border-border pt-4">
<div className="space-y-3">
<PlanGuideItem
tier="Silver"
icon={<WrenchScrewdriverIcon className="h-5 w-5" />}
title="Silver"
idealFor="Tech-savvy users with their own router"
description="You get the NTT modem and ISP connection. Bring your own WiFi router and configure the network yourself."
/>
<PlanGuideItem
tier="Gold"
icon={<SparklesIcon className="h-5 w-5" />}
title="Gold"
idealFor="Most customers—hassle-free setup"
description="We provide everything: NTT modem, WiFi router, and pre-configured ISP. Just plug in and connect. Optional range extender available if needed."
highlight
/>
<PlanGuideItem
tier="Gold"
icon={<SparklesIcon className="h-5 w-5" />}
title="Gold"
idealFor="Most customers—hassle-free setup"
description="We provide everything: NTT modem, WiFi router, and pre-configured ISP. Just plug in and connect. Optional range extender available."
highlight
/>
<PlanGuideItem
tier="Platinum"
icon={<CubeTransparentIcon className="h-5 w-5" />}
title="Platinum"
idealFor="Larger homes needing custom coverage"
description="For residences 50m²+ where one router isn't enough. We design a custom mesh network with Netgear INSIGHT routers, cloud management, and professional setup."
/>
</div>
<PlanGuideItem
tier="Platinum"
icon={<CubeTransparentIcon className="h-5 w-5" />}
title="Platinum"
idealFor="Larger homes needing custom coverage"
description="For residences where one router isn't enough. We design a custom mesh network with Netgear INSIGHT routers, cloud management, and professional setup."
/>
</div>
{/* Platinum additional info */}
<div className="mt-4 p-4 bg-info-soft/30 border border-primary/20 rounded-xl">
<p className="text-sm text-foreground">
<span className="font-semibold text-primary">About Platinum plans:</span> After verifying
your address, we&apos;ll assess your space and create a tailored proposal. This may
include multiple mesh routers, LAN wiring, or other equipment based on your layout and
needs. Final pricing depends on your specific setup requirements.
</p>
</div>
<div className="mt-4 p-3 bg-info-soft/30 border border-primary/20 rounded-lg">
<p className="text-sm text-muted-foreground">
<span className="font-semibold text-primary">About Platinum:</span> After verifying
your address, we'll assess your space and create a tailored proposal. Final pricing
depends on your specific setup requirements.
</p>
</div>
</div>
)}
</section>
);
}

View File

@ -36,6 +36,8 @@ interface PublicOfferingCardProps {
defaultExpanded?: boolean;
/** Show info tooltip explaining connection types (for Apartment) */
showConnectionInfo?: boolean;
customCtaLabel?: string;
onCtaClick?: (e: React.MouseEvent) => void;
}
const tierStyles = {
@ -124,6 +126,8 @@ export function PublicOfferingCard({
ctaPath,
defaultExpanded = false,
showConnectionInfo = false,
customCtaLabel,
onCtaClick,
}: PublicOfferingCardProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [showInfo, setShowInfo] = useState(false);
@ -259,9 +263,15 @@ export function PublicOfferingCard({
</span>{" "}
(or 12/24-month installment)
</p>
<Button as="a" href={ctaPath} size="sm" className="whitespace-nowrap">
Check availability
</Button>
{onCtaClick ? (
<Button as="button" onClick={onCtaClick} size="sm" className="whitespace-nowrap">
{customCtaLabel ?? "Check availability"}
</Button>
) : (
<Button as="a" href={ctaPath} size="sm" className="whitespace-nowrap">
{customCtaLabel ?? "Check availability"}
</Button>
)}
</div>
</div>
)}

View File

@ -1,81 +0,0 @@
"use client";
import {
WifiIcon,
GlobeAltIcon,
WrenchScrewdriverIcon,
ChatBubbleLeftRightIcon,
UserGroupIcon,
HomeModernIcon,
} from "@heroicons/react/24/outline";
interface FeatureItemProps {
icon: React.ReactNode;
title: string;
description: string;
}
function FeatureItem({ icon, title, description }: FeatureItemProps) {
return (
<div className="flex items-start gap-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary border border-primary/20 flex-shrink-0">
{icon}
</div>
<div>
<h4 className="font-semibold text-foreground mb-1">{title}</h4>
<p className="text-sm text-muted-foreground leading-relaxed">{description}</p>
</div>
</div>
);
}
export function WhyChooseSection() {
return (
<section className="bg-card rounded-2xl border border-border shadow-[var(--cp-shadow-1)] p-6 sm:p-8">
<div className="mb-6">
<h3 className="text-xl font-bold text-foreground mb-2">Why choose our internet service?</h3>
<p className="text-sm text-muted-foreground">
Japan&apos;s most reliable fiber network with dedicated English support.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<FeatureItem
icon={<WifiIcon className="h-5 w-5" />}
title="NTT Fiber Network"
description="Powered by Japan's largest and most reliable optical fiber infrastructure, delivering speeds up to 10Gbps."
/>
<FeatureItem
icon={<GlobeAltIcon className="h-5 w-5" />}
title="IPoE Connection"
description="Modern IPv6/IPoE technology for congestion-free access, even during peak hours. PPPoE also available."
/>
<FeatureItem
icon={<WrenchScrewdriverIcon className="h-5 w-5" />}
title="Flexible ISP Options"
description="Multiple connection protocols within a single contract. Switch between IPoE and PPPoE as needed."
/>
<FeatureItem
icon={<HomeModernIcon className="h-5 w-5" />}
title="One-Stop Solution"
description="NTT line, ISP service, and optional equipment—all managed through one provider. One bill, one contact point."
/>
<FeatureItem
icon={<ChatBubbleLeftRightIcon className="h-5 w-5" />}
title="Full English Support"
description="Native English customer service for setup, billing questions, and technical support. No language barriers."
/>
<FeatureItem
icon={<UserGroupIcon className="h-5 w-5" />}
title="On-Site Assistance"
description="Need help at home? Our technicians can visit for setup, troubleshooting, or network optimization."
/>
</div>
</section>
);
}

View File

@ -1,106 +0,0 @@
"use client";
import {
WifiIcon,
ChatBubbleLeftRightIcon,
BoltIcon,
WrenchScrewdriverIcon,
DocumentTextIcon,
GlobeAltIcon,
} from "@heroicons/react/24/outline";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
interface FeatureProps {
icon: React.ReactNode;
title: string;
description: string;
highlight?: string;
}
function FeatureCard({ icon, title, description, highlight }: FeatureProps) {
return (
<div className="flex items-start gap-3 p-4">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-primary/15 to-primary/5 text-primary flex-shrink-0">
{icon}
</div>
<div className="min-w-0 flex-1">
<h3 className="font-semibold text-foreground text-sm leading-tight">{title}</h3>
<p className="text-xs text-muted-foreground leading-relaxed mt-0.5">{description}</p>
{highlight && (
<span className="inline-flex items-center gap-1 mt-1.5 text-[10px] font-medium text-success">
<CheckCircleIcon className="h-3 w-3" />
{highlight}
</span>
)}
</div>
</div>
);
}
/**
* Why Choose Us - Clean feature grid
* 6 key differentiators in a 3x2 grid on desktop, 2x3 on tablet, stacked on mobile
*/
export function WhyChooseUsPillars() {
const features: FeatureProps[] = [
{
icon: <WifiIcon className="h-[18px] w-[18px]" />,
title: "NTT Optical Fiber",
description: "Japan's most reliable network with speeds up to 10Gbps",
highlight: "99.9% uptime",
},
{
icon: <BoltIcon className="h-[18px] w-[18px]" />,
title: "IPv6/IPoE Ready",
description: "Next-gen protocol for congestion-free browsing",
highlight: "No peak-hour slowdowns",
},
{
icon: <ChatBubbleLeftRightIcon className="h-[18px] w-[18px]" />,
title: "Full English Support",
description: "Native English service for setup, billing & technical help",
highlight: "No language barriers",
},
{
icon: <DocumentTextIcon className="h-[18px] w-[18px]" />,
title: "One Bill, One Provider",
description: "NTT line + ISP + equipment bundled with simple billing",
highlight: "No hidden fees",
},
{
icon: <WrenchScrewdriverIcon className="h-[18px] w-[18px]" />,
title: "On-site Support",
description: "Technicians can visit for installation & troubleshooting",
highlight: "Professional setup",
},
{
icon: <GlobeAltIcon className="h-[18px] w-[18px]" />,
title: "Flexible Options",
description: "Multiple ISP configs available, IPv4/PPPoE if needed",
highlight: "Customizable",
},
];
return (
<div className="rounded-2xl border border-border bg-card shadow-[var(--cp-shadow-1)] overflow-hidden">
{/* Header */}
<div className="px-4 py-2.5 border-b border-border bg-muted/30">
<h2 className="text-sm font-bold text-foreground text-center tracking-tight">
Why Choose Us
</h2>
</div>
{/* Feature grid - 3 columns on large, 2 on medium, 1 on mobile */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{features.map((feature, index) => (
<div
key={index}
className="border-b border-border sm:border-b-0 sm:border-r last:border-b-0 sm:last:border-r-0 sm:[&:nth-child(2n)]:border-r-0 lg:[&:nth-child(2n)]:border-r lg:[&:nth-child(3n)]:border-r-0 sm:[&:nth-child(n+3)]:border-t lg:[&:nth-child(n+3)]:border-t-0 lg:[&:nth-child(n+4)]:border-t"
>
<FeatureCard {...feature} />
</div>
))}
</div>
</div>
);
}

View File

@ -16,7 +16,7 @@ import { InstallationStep } from "./steps/InstallationStep";
import { AddonsStep } from "./steps/AddonsStep";
import { ReviewOrderStep } from "./steps/ReviewOrderStep";
import { useConfigureState } from "./hooks/useConfigureState";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import { PlanHeader } from "@/features/catalog/components/internet/PlanHeader";
interface Props {
@ -58,7 +58,7 @@ export function InternetConfigureContainer({
currentStep,
setCurrentStep,
}: Props) {
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const [renderedStep, setRenderedStep] = useState(currentStep);
const [transitionPhase, setTransitionPhase] = useState<"idle" | "enter" | "exit">("idle");
// Use local state ONLY for step validation, step management now in Zustand
@ -214,7 +214,7 @@ export function InternetConfigureContainer({
{/* Plan Header */}
<PlanHeader
plan={plan}
backHref={`${shopBasePath}/internet`}
backHref={`${servicesBasePath}/internet`}
backLabel="Back to Internet Plans"
/>

View File

@ -0,0 +1,168 @@
"use client";
import { useState } from "react";
import { PhoneIcon, ChatBubbleLeftIcon, GlobeAltIcon } from "@heroicons/react/24/outline";
import { ChevronDownIcon } from "@heroicons/react/24/solid";
const domesticRates = {
calling: { rate: 10, unit: "30 sec" },
sms: { rate: 3, unit: "message" },
};
const internationalSmsRate = 100; // per message
const internationalCallingRates = [
{ country: "United States", code: "US", rate: "31-34" },
{ country: "United Kingdom", code: "UK", rate: "78-108" },
{ country: "Australia", code: "AU", rate: "63-68" },
{ country: "China", code: "CN", rate: "49-57" },
{ country: "India", code: "IN", rate: "98-148" },
{ country: "Singapore", code: "SG", rate: "63-68" },
{ country: "France", code: "FR", rate: "78-108" },
{ country: "Germany", code: "DE", rate: "78-108" },
];
export function SimCallingRates() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="bg-card border border-border rounded-2xl overflow-hidden">
{/* Header */}
<div className="p-6 border-b border-border">
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
<PhoneIcon className="w-5 h-5 text-primary" />
Calling & SMS Rates
</h3>
<p className="text-sm text-muted-foreground mt-1">
Pay-per-use charges apply. Billed 5-6 weeks after usage.
</p>
</div>
{/* Domestic Rates */}
<div className="p-6 bg-muted/30">
<h4 className="text-sm font-medium text-foreground mb-4 flex items-center gap-2">
<span className="w-6 h-4 rounded-sm bg-[#BC002D] relative overflow-hidden flex items-center justify-center">
<span className="w-3 h-3 rounded-full bg-white" />
</span>
Domestic (Japan)
</h4>
<div className="grid grid-cols-2 gap-4">
<div className="bg-card rounded-lg p-4 border border-border">
<div className="flex items-center gap-2 mb-2">
<PhoneIcon className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Voice Calls</span>
</div>
<div className="text-2xl font-bold text-foreground">
¥{domesticRates.calling.rate}
<span className="text-sm font-normal text-muted-foreground ml-1">
/{domesticRates.calling.unit}
</span>
</div>
</div>
<div className="bg-card rounded-lg p-4 border border-border">
<div className="flex items-center gap-2 mb-2">
<ChatBubbleLeftIcon className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">SMS</span>
</div>
<div className="text-2xl font-bold text-foreground">
¥{domesticRates.sms.rate}
<span className="text-sm font-normal text-muted-foreground ml-1">
/{domesticRates.sms.unit}
</span>
</div>
</div>
</div>
<p className="text-xs text-muted-foreground mt-3">Incoming calls and SMS are free.</p>
</div>
{/* International Rates (Collapsible) */}
<div className="border-t border-border">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full p-4 flex items-center justify-between text-left hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-2">
<GlobeAltIcon className="w-5 h-5 text-primary" />
<span className="font-medium text-foreground">International Calling Rates</span>
</div>
<ChevronDownIcon
className={`w-5 h-5 text-muted-foreground transition-transform ${isExpanded ? "rotate-180" : ""}`}
/>
</button>
{isExpanded && (
<div className="px-6 pb-6">
<div className="bg-muted/30 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left p-3 font-medium text-muted-foreground">Country</th>
<th className="text-right p-3 font-medium text-muted-foreground">
Rate (¥/30sec)
</th>
</tr>
</thead>
<tbody>
{internationalCallingRates.map((rate, index) => (
<tr
key={rate.code}
className={
index < internationalCallingRates.length - 1
? "border-b border-border/50"
: ""
}
>
<td className="p-3 text-foreground">
{rate.country}
<span className="text-muted-foreground ml-1">({rate.code})</span>
</td>
<td className="p-3 text-right font-medium text-foreground">¥{rate.rate}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-4 space-y-2 text-xs text-muted-foreground">
<p> International SMS: ¥{internationalSmsRate}/message</p>
<p> Rates vary by time of day and day of week</p>
<p>
For full rate details, visit{" "}
<a
href="https://www.docomo.ne.jp/service/world/worldcall/call/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
NTT Docomo's website
</a>
</p>
</div>
</div>
)}
</div>
{/* Unlimited Calling Option */}
<div className="p-6 bg-success/5 border-t border-success/20">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-success/10 flex items-center justify-center flex-shrink-0">
<PhoneIcon className="w-5 h-5 text-success" />
</div>
<div>
<h4 className="font-medium text-foreground">Unlimited Domestic Calling</h4>
<p className="text-sm text-muted-foreground mt-1">
Add unlimited domestic calls to any Data+Voice plan for{" "}
<span className="font-semibold text-success">¥3,000/month</span>
</p>
<p className="text-xs text-muted-foreground mt-2">
Available as an add-on during checkout. International calls not included.
</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -10,7 +10,7 @@ import { ActivationForm } from "@/features/catalog/components/sim/ActivationForm
import { MnpForm } from "@/features/catalog/components/sim/MnpForm";
import { ProgressSteps } from "@/components/molecules";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import {
ArrowLeftIcon,
ArrowRightIcon,
@ -49,7 +49,7 @@ export function SimConfigureView({
setCurrentStep,
onConfirm,
}: Props) {
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const getRequiredActivationFee = (
fees: SimActivationFeeCatalogItem[]
): SimActivationFeeCatalogItem | undefined => {
@ -164,7 +164,7 @@ export function SimConfigureView({
<h2 className="text-xl font-semibold text-foreground mb-2">Plan Not Found</h2>
<p className="text-muted-foreground mb-4">The selected plan could not be found</p>
<a
href={`${shopBasePath}/sim`}
href={`${servicesBasePath}/sim`}
className="text-primary hover:text-primary-hover font-medium"
>
Return to SIM Plans
@ -190,7 +190,7 @@ export function SimConfigureView({
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="max-w-4xl mx-auto space-y-8">
<CatalogBackLink href={`${shopBasePath}/sim`} label="Back to SIM Plans" />
<CatalogBackLink href={`${servicesBasePath}/sim`} label="Back to SIM Plans" />
<AnimatedCard variant="static" className="p-6">
<div className="flex justify-between items-start">
@ -535,10 +535,24 @@ export function SimConfigureView({
</span>
</div>
)}
<p className="text-xs text-muted-foreground pt-2">
Prices exclude 10% consumption tax
</p>
</div>
</div>
</div>
{/* Verification notice */}
<div className="max-w-lg mx-auto mb-6 bg-info/10 border border-info/25 rounded-lg p-4">
<p className="text-sm text-foreground">
<span className="font-medium">Next steps after checkout:</span>{" "}
<span className="text-muted-foreground">
We'll review your order and ID verification within 1-2 business days. You'll
receive an email once approved.
</span>
</p>
</div>
<div className="flex justify-between items-center pt-6 border-t border-border">
<Button
onClick={() => setCurrentStep(4)}

View File

@ -0,0 +1,115 @@
"use client";
import { BanknotesIcon, UsersIcon, ArrowPathIcon } from "@heroicons/react/24/outline";
const fees = [
{
title: "Activation Fee",
amount: 1500,
type: "one-time" as const,
description: "Required for all new SIM activations",
icon: BanknotesIcon,
},
{
title: "SIM Reissue",
amount: 1500,
type: "one-time" as const,
description: "For lost, damaged, or replacement SIM cards",
icon: ArrowPathIcon,
},
{
title: "eSIM Re-download",
amount: 1500,
type: "one-time" as const,
description: "If eSIM download is interrupted or deleted",
icon: ArrowPathIcon,
},
];
const discounts = [
{
title: "Multi-SIM Discount",
amount: -300,
type: "monthly" as const,
description: "Per additional Voice SIM on your account",
icon: UsersIcon,
},
];
export function SimFees() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* One-time Fees */}
<div className="bg-card border border-border rounded-2xl p-6">
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<BanknotesIcon className="w-5 h-5 text-warning" />
One-time Fees
</h3>
<div className="space-y-4">
{fees.map(fee => (
<div
key={fee.title}
className="flex items-start gap-3 pb-4 border-b border-border last:border-0 last:pb-0"
>
<div className="w-10 h-10 rounded-lg bg-warning/10 flex items-center justify-center flex-shrink-0">
<fee.icon className="w-5 h-5 text-warning" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-baseline justify-between gap-2">
<h4 className="font-medium text-foreground">{fee.title}</h4>
<span className="font-bold text-foreground whitespace-nowrap">
¥{fee.amount.toLocaleString()}
</span>
</div>
<p className="text-sm text-muted-foreground mt-0.5">{fee.description}</p>
</div>
</div>
))}
</div>
<p className="text-xs text-muted-foreground mt-4 pt-4 border-t border-border">
All prices exclude 10% consumption tax
</p>
</div>
{/* Discounts */}
<div className="bg-card border border-border rounded-2xl p-6">
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<UsersIcon className="w-5 h-5 text-success" />
Available Discounts
</h3>
<div className="space-y-4">
{discounts.map(discount => (
<div
key={discount.title}
className="flex items-start gap-3 p-4 bg-success/5 border border-success/20 rounded-xl"
>
<div className="w-10 h-10 rounded-lg bg-success/10 flex items-center justify-center flex-shrink-0">
<discount.icon className="w-5 h-5 text-success" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-baseline justify-between gap-2">
<h4 className="font-medium text-foreground">{discount.title}</h4>
<span className="font-bold text-success whitespace-nowrap">
¥{discount.amount.toLocaleString()}/mo
</span>
</div>
<p className="text-sm text-muted-foreground mt-0.5">{discount.description}</p>
</div>
</div>
))}
</div>
<div className="mt-6 p-4 bg-muted/50 rounded-lg">
<h4 className="font-medium text-foreground text-sm mb-2">First Month Free</h4>
<p className="text-sm text-muted-foreground">
Basic fee from sign-up day to end of month is waived. Billing starts on the 1st of the
following month.
</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,71 @@
"use client";
import {
ClipboardDocumentListIcon,
CreditCardIcon,
ShieldCheckIcon,
DevicePhoneMobileIcon,
} from "@heroicons/react/24/outline";
const steps = [
{
number: 1,
title: "Select Plan",
description: "Choose your data, voice, or combo plan",
icon: ClipboardDocumentListIcon,
},
{
number: 2,
title: "Submit Order",
description: "Add payment method & residence card",
icon: CreditCardIcon,
},
{
number: 3,
title: "Verification",
description: "We review your ID (1-2 business days)",
icon: ShieldCheckIcon,
},
{
number: 4,
title: "Receive SIM",
description: "eSIM via email or physical SIM shipped",
icon: DevicePhoneMobileIcon,
},
];
export function SimOrderProcess() {
return (
<div className="bg-card border border-border rounded-2xl p-6 md:p-8">
<h3 className="text-lg font-semibold text-foreground text-center mb-6">How Ordering Works</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6">
{steps.map((step, index) => (
<div key={step.number} className="relative text-center">
{/* Connector line (hidden on mobile between rows) */}
{index < steps.length - 1 && (
<div className="hidden md:block absolute top-8 left-[60%] w-[80%] h-0.5 bg-border" />
)}
{/* Step icon */}
<div className="relative mx-auto w-16 h-16 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center mb-3">
<step.icon className="w-7 h-7 text-primary" />
<span className="absolute -top-2 -right-2 w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs font-bold flex items-center justify-center">
{step.number}
</span>
</div>
{/* Step content */}
<h4 className="font-medium text-foreground text-sm mb-1">{step.title}</h4>
<p className="text-xs text-muted-foreground leading-relaxed">{step.description}</p>
</div>
))}
</div>
<p className="text-center text-xs text-muted-foreground mt-6 pt-4 border-t border-border">
ID verification is required for all new SIM subscriptions in compliance with Japanese
telecommunications regulations.
</p>
</div>
);
}

View File

@ -9,7 +9,7 @@ import { CardPricing } from "@/features/catalog/components/base/CardPricing";
import { CardBadge } from "@/features/catalog/components/base/CardBadge";
import { useRouter } from "next/navigation";
import { useCatalogStore } from "@/features/catalog/services/catalog.store";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
export type SimPlanCardAction = { label: string; href: string };
export type SimPlanCardActionResolver =
@ -32,7 +32,7 @@ export function SimPlanCard({
const displayPrice = plan.monthlyPrice ?? plan.unitPrice ?? plan.oneTimePrice ?? 0;
const isFamilyPlan = isFamily ?? Boolean(plan.simHasFamilyDiscount);
const router = useRouter();
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const resolvedAction = typeof action === "function" ? action(plan) : action;
return (
@ -77,7 +77,7 @@ export function SimPlanCard({
const { resetSimConfig, setSimConfig } = useCatalogStore.getState();
resetSimConfig();
setSimConfig({ planSku: plan.sku, currentStep: 1 });
router.push(`${shopBasePath}/sim/configure?planSku=${encodeURIComponent(plan.sku)}`);
router.push(`${servicesBasePath}/sim/configure?planSku=${encodeURIComponent(plan.sku)}`);
}}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>

View File

@ -0,0 +1,157 @@
"use client";
import { useState } from "react";
import {
DevicePhoneMobileIcon,
SignalIcon,
TruckIcon,
EnvelopeIcon,
QuestionMarkCircleIcon,
CheckIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
const esimFeatures = [
{ text: "No physical card needed", included: true },
{ text: "Delivered via email after approval", included: true },
{ text: "EID number required", included: true, note: true },
{ text: "Can be transferred between devices", included: false },
];
const physicalSimFeatures = [
{ text: "Works with any unlocked device", included: true },
{ text: "Ships after approval (1-3 days)", included: true },
{ text: "3-in-1 size (Nano/Micro/Standard)", included: true },
{ text: "No EID required", included: true },
];
const compatibleEidPrefixes = ["89049032", "89033023", "89033024", "89043051", "89043052"];
export function SimTypeComparison() {
const [showEidInfo, setShowEidInfo] = useState(false);
return (
<div className="bg-card border border-border rounded-2xl p-6 md:p-8">
<h3 className="text-lg font-semibold text-foreground text-center mb-2">
eSIM vs Physical SIM
</h3>
<p className="text-sm text-muted-foreground text-center mb-6">
Choose the right option for your device
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* eSIM Card */}
<div className="border border-primary/30 bg-primary/5 rounded-xl p-5">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center">
<SignalIcon className="w-6 h-6 text-primary" />
</div>
<div>
<h4 className="font-semibold text-foreground">eSIM</h4>
<p className="text-xs text-muted-foreground">Digital SIM card</p>
</div>
</div>
<ul className="space-y-3">
{esimFeatures.map((feature, index) => (
<li key={index} className="flex items-start gap-2">
{feature.included ? (
<CheckIcon className="w-4 h-4 text-success mt-0.5 flex-shrink-0" />
) : (
<span className="w-4 h-4 mt-0.5 flex-shrink-0 text-center text-muted-foreground">
</span>
)}
<span className="text-sm text-foreground">
{feature.text}
{feature.note && (
<button
onClick={() => setShowEidInfo(!showEidInfo)}
className="ml-1 text-primary hover:text-primary-hover"
>
<QuestionMarkCircleIcon className="w-4 h-4 inline" />
</button>
)}
</span>
</li>
))}
</ul>
<div className="mt-4 pt-4 border-t border-primary/20">
<div className="flex items-center gap-2 text-sm">
<EnvelopeIcon className="w-4 h-4 text-primary" />
<span className="text-muted-foreground">
Delivery: <span className="text-foreground font-medium">Email after approval</span>
</span>
</div>
</div>
</div>
{/* Physical SIM Card */}
<div className="border border-border bg-muted/30 rounded-xl p-5">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-foreground/10 flex items-center justify-center">
<DevicePhoneMobileIcon className="w-6 h-6 text-foreground" />
</div>
<div>
<h4 className="font-semibold text-foreground">Physical SIM</h4>
<p className="text-xs text-muted-foreground">Traditional SIM card</p>
</div>
</div>
<ul className="space-y-3">
{physicalSimFeatures.map((feature, index) => (
<li key={index} className="flex items-start gap-2">
<CheckIcon className="w-4 h-4 text-success mt-0.5 flex-shrink-0" />
<span className="text-sm text-foreground">{feature.text}</span>
</li>
))}
</ul>
<div className="mt-4 pt-4 border-t border-border">
<div className="flex items-center gap-2 text-sm">
<TruckIcon className="w-4 h-4 text-muted-foreground" />
<span className="text-muted-foreground">
Delivery: <span className="text-foreground font-medium">1-3 business days</span>
</span>
</div>
</div>
</div>
</div>
{/* EID Info Panel */}
{showEidInfo && (
<div className="mt-6 p-4 bg-info/10 border border-info/25 rounded-xl">
<div className="flex items-start gap-3">
<InformationCircleIcon className="w-5 h-5 text-info mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium text-foreground mb-2">What is an EID?</h4>
<p className="text-sm text-muted-foreground mb-3">
An EID (Embedded Identity Document) is a 32-digit number unique to your device's
eSIM chip. You can find it in your phone's settings under "About" or "SIM status".
</p>
<div className="text-sm">
<p className="font-medium text-foreground mb-1">Compatible EID prefixes:</p>
<div className="flex flex-wrap gap-2">
{compatibleEidPrefixes.map(prefix => (
<code
key={prefix}
className="px-2 py-1 bg-card rounded text-xs font-mono text-foreground"
>
{prefix}...
</code>
))}
</div>
</div>
</div>
</div>
</div>
)}
{/* Note */}
<p className="text-center text-xs text-muted-foreground mt-6">
Both options require ID verification before activation (1-2 business days)
</p>
</div>
);
}

View File

@ -1,4 +1,14 @@
import { DevicePhoneMobileIcon, CpuChipIcon } from "@heroicons/react/24/outline";
"use client";
import { useState } from "react";
import {
DevicePhoneMobileIcon,
SignalIcon,
TruckIcon,
EnvelopeIcon,
InformationCircleIcon,
CheckIcon,
} from "@heroicons/react/24/outline";
interface SimTypeSelectorProps {
simType: "Physical SIM" | "eSIM" | "";
@ -8,6 +18,8 @@ interface SimTypeSelectorProps {
errors: Record<string, string | undefined>;
}
const compatibleEidPrefixes = ["89049032", "89033023", "89033024", "89043051", "89043052"];
export function SimTypeSelector({
simType,
onSimTypeChange,
@ -15,88 +27,203 @@ export function SimTypeSelector({
onEidChange,
errors,
}: SimTypeSelectorProps) {
return (
<div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-colors duration-200 ease-in-out ${
simType === "Physical SIM"
? "border-blue-500 bg-blue-50 shadow-sm ring-1 ring-blue-200"
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
}`}
>
<input
type="radio"
name="simType"
value="Physical SIM"
checked={simType === "Physical SIM"}
onChange={e => onSimTypeChange(e.target.value as "Physical SIM")}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
<DevicePhoneMobileIcon className="w-6 h-6 text-blue-600 mt-0.5 flex-shrink-0" />
<div>
<span className="font-medium text-gray-900">Physical SIM</span>
<p className="text-sm text-gray-600 mt-1">
Traditional SIM card that will be mailed to you. Compatible with all devices.
</p>
</div>
</label>
const [showEidInfo, setShowEidInfo] = useState(false);
<label
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-colors duration-200 ease-in-out ${
return (
<div className="space-y-6">
{/* SIM Type Selection Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* eSIM Option */}
<button
type="button"
onClick={() => onSimTypeChange("eSIM")}
className={`relative text-left p-5 rounded-xl border-2 transition-all duration-200 ${
simType === "eSIM"
? "border-blue-500 bg-blue-50 shadow-sm ring-1 ring-blue-200"
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
? "border-primary bg-primary/5 ring-1 ring-primary/20"
: "border-border bg-card hover:border-primary/40 hover:bg-muted/50"
}`}
>
<input
type="radio"
name="simType"
value="eSIM"
checked={simType === "eSIM"}
onChange={e => onSimTypeChange(e.target.value as "eSIM")}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
<CpuChipIcon className="w-6 h-6 text-blue-600 mt-0.5 flex-shrink-0" />
<div>
<span className="font-medium text-gray-900">eSIM (Digital)</span>
<p className="text-sm text-gray-600 mt-1">
Digital SIM activated instantly. Requires eSIM-compatible device and EID number.
</p>
{simType === "eSIM" && (
<div className="absolute top-3 right-3 w-6 h-6 rounded-full bg-primary flex items-center justify-center">
<CheckIcon className="w-4 h-4 text-primary-foreground" />
</div>
)}
<div className="flex items-start gap-4">
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
simType === "eSIM" ? "bg-primary/10" : "bg-muted"
}`}
>
<SignalIcon
className={`w-6 h-6 ${simType === "eSIM" ? "text-primary" : "text-muted-foreground"}`}
/>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-foreground mb-1">eSIM (Digital)</h4>
<p className="text-sm text-muted-foreground mb-3">
Digital SIM delivered via email after approval
</p>
<div className="flex items-center gap-2 text-sm">
<EnvelopeIcon className="w-4 h-4 text-primary" />
<span className="text-muted-foreground">
Delivery:{" "}
<span className="text-foreground font-medium">Email after approval</span>
</span>
</div>
</div>
</div>
</label>
<ul className="mt-4 space-y-2 text-sm text-muted-foreground">
<li className="flex items-center gap-2">
<CheckIcon className="w-4 h-4 text-success flex-shrink-0" />
<span>No physical card needed</span>
</li>
<li className="flex items-center gap-2">
<CheckIcon className="w-4 h-4 text-success flex-shrink-0" />
<span>EID number required</span>
</li>
</ul>
</button>
{/* Physical SIM Option */}
<button
type="button"
onClick={() => onSimTypeChange("Physical SIM")}
className={`relative text-left p-5 rounded-xl border-2 transition-all duration-200 ${
simType === "Physical SIM"
? "border-primary bg-primary/5 ring-1 ring-primary/20"
: "border-border bg-card hover:border-primary/40 hover:bg-muted/50"
}`}
>
{simType === "Physical SIM" && (
<div className="absolute top-3 right-3 w-6 h-6 rounded-full bg-primary flex items-center justify-center">
<CheckIcon className="w-4 h-4 text-primary-foreground" />
</div>
)}
<div className="flex items-start gap-4">
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
simType === "Physical SIM" ? "bg-primary/10" : "bg-muted"
}`}
>
<DevicePhoneMobileIcon
className={`w-6 h-6 ${simType === "Physical SIM" ? "text-primary" : "text-muted-foreground"}`}
/>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-foreground mb-1">Physical SIM</h4>
<p className="text-sm text-muted-foreground mb-3">
Traditional SIM card shipped to your address
</p>
<div className="flex items-center gap-2 text-sm">
<TruckIcon className="w-4 h-4 text-muted-foreground" />
<span className="text-muted-foreground">
Delivery: <span className="text-foreground font-medium">1-3 business days</span>
</span>
</div>
</div>
</div>
<ul className="mt-4 space-y-2 text-sm text-muted-foreground">
<li className="flex items-center gap-2">
<CheckIcon className="w-4 h-4 text-success flex-shrink-0" />
<span>Works with any unlocked device</span>
</li>
<li className="flex items-center gap-2">
<CheckIcon className="w-4 h-4 text-success flex-shrink-0" />
<span>3-in-1 size (Nano/Micro/Standard)</span>
</li>
</ul>
</button>
</div>
{/* EID Input for eSIM */}
<div
className={`overflow-hidden transition-[max-height,opacity] duration-300 ease-out ${
simType === "eSIM" ? "max-h-[360px] opacity-100" : "max-h-0 opacity-0"
className={`overflow-hidden transition-all duration-300 ease-out ${
simType === "eSIM" ? "max-h-[400px] opacity-100" : "max-h-0 opacity-0"
}`}
aria-hidden={simType !== "eSIM"}
>
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
<h4 className="font-medium text-blue-900 mb-2">eSIM Device Information</h4>
<div className="p-5 bg-primary/5 border border-primary/20 rounded-xl">
<div className="flex items-start gap-3 mb-4">
<InformationCircleIcon className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium text-foreground mb-1">eSIM Device Information</h4>
<p className="text-sm text-muted-foreground">
Your EID (Embedded Identity Document) is required to provision the eSIM to your
device.
</p>
</div>
</div>
<div>
<label htmlFor="eid" className="block text-sm font-medium text-gray-700 mb-1">
EID (Embedded Identity Document) *
<label htmlFor="eid" className="block text-sm font-medium text-foreground mb-2">
EID Number <span className="text-destructive">*</span>
</label>
<input
type="text"
id="eid"
value={eid}
onChange={e => onEidChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
className={`w-full px-4 py-3 bg-card border rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors ${
errors.eid ? "border-destructive" : "border-border"
}`}
placeholder="32-digit EID number"
maxLength={32}
/>
{errors.eid && <p className="text-red-600 text-sm mt-1">{errors.eid}</p>}
<p className="text-xs text-blue-700 mt-1">
Find your EID in: Settings General About EID (iOS) or Settings About Phone
IMEI (Android)
</p>
{errors.eid && <p className="text-destructive text-sm mt-2">{errors.eid}</p>}
<button
type="button"
onClick={() => setShowEidInfo(!showEidInfo)}
className="text-sm text-primary hover:underline mt-2"
>
{showEidInfo ? "Hide" : "Where to find your EID?"}
</button>
{showEidInfo && (
<div className="mt-3 p-4 bg-card border border-border rounded-lg text-sm">
<p className="text-muted-foreground mb-3">
Find your EID in your phone's settings:
</p>
<ul className="space-y-2 text-muted-foreground">
<li>
<strong className="text-foreground">iOS:</strong> Settings General About
EID
</li>
<li>
<strong className="text-foreground">Android:</strong> Settings About Phone
SIM status EID
</li>
</ul>
<div className="mt-4 pt-3 border-t border-border">
<p className="font-medium text-foreground mb-2">Compatible EID prefixes:</p>
<div className="flex flex-wrap gap-2">
{compatibleEidPrefixes.map(prefix => (
<code
key={prefix}
className="px-2 py-1 bg-muted rounded text-xs font-mono text-foreground"
>
{prefix}...
</code>
))}
</div>
</div>
</div>
)}
</div>
</div>
</div>
{/* Note about verification */}
<div className="flex items-start gap-3 p-4 bg-muted/50 border border-border rounded-lg">
<InformationCircleIcon className="w-5 h-5 text-muted-foreground mt-0.5 flex-shrink-0" />
<p className="text-sm text-muted-foreground">
Both eSIM and Physical SIM require ID verification before activation (1-2 business days).
</p>
</div>
</div>
);
}

View File

@ -3,3 +3,4 @@ export * from "./useConfigureParams";
export * from "./useSimConfigure";
export * from "./useInternetConfigure";
export * from "./useInternetEligibility";
export * from "./useServicesBasePath";

View File

@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useInternetCatalog, useInternetPlan } from ".";
import { useCatalogStore } from "../services/catalog.store";
import { useShopBasePath } from "./useShopBasePath";
import { useServicesBasePath } from "./useServicesBasePath";
import type { AccessModeValue } from "@customer-portal/domain/orders";
import type {
InternetPlanCatalogItem,
@ -42,7 +42,7 @@ export type UseInternetConfigureResult = {
*/
export function useInternetConfigure(): UseInternetConfigureResult {
const router = useRouter();
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const searchParams = useSearchParams();
const paramsSignature = useMemo(() => searchParams.toString(), [searchParams]);
const urlPlanSku = searchParams.get("planSku");
@ -77,7 +77,7 @@ export function useInternetConfigure(): UseInternetConfigureResult {
// Redirect if no plan selected
if (!urlPlanSku && !configState.planSku) {
router.push(`${shopBasePath}/internet`);
router.push(`${servicesBasePath}/internet`);
}
}, [
configState.planSku,
@ -85,7 +85,7 @@ export function useInternetConfigure(): UseInternetConfigureResult {
restoreFromParams,
router,
setConfig,
shopBasePath,
servicesBasePath,
urlPlanSku,
]);

View File

@ -1,17 +0,0 @@
"use client";
import { usePathname } from "next/navigation";
/**
* Returns the active shop base path for the current shell.
*
* - Public shop: `/shop`
* - Account shop (inside AppShell): `/account/shop`
*/
export function useShopBasePath(): "/shop" | "/account/shop" {
const pathname = usePathname();
if (pathname.startsWith("/account/shop")) {
return "/account/shop";
}
return "/shop";
}

View File

@ -4,7 +4,7 @@ import { useEffect, useCallback, useMemo, useRef } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useSimCatalog, useSimPlan } from ".";
import { useCatalogStore } from "../services/catalog.store";
import { useShopBasePath } from "./useShopBasePath";
import { useServicesBasePath } from "./useServicesBasePath";
import {
simConfigureFormSchema,
type SimConfigureFormData,
@ -55,7 +55,7 @@ export type UseSimConfigureResult = {
*/
export function useSimConfigure(planId?: string): UseSimConfigureResult {
const router = useRouter();
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const searchParams = useSearchParams();
const urlPlanSku = searchParams.get("planSku");
const paramsSignature = useMemo(() => searchParams.toString(), [searchParams]);
@ -91,7 +91,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
// Redirect if no plan selected
if (!effectivePlanSku && !configState.planSku) {
router.push(`${shopBasePath}/sim`);
router.push(`${servicesBasePath}/sim`);
}
}, [
configState.planSku,
@ -100,7 +100,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
restoreFromParams,
router,
setConfig,
shopBasePath,
servicesBasePath,
urlPlanSku,
]);

View File

@ -13,10 +13,10 @@ import {
} from "@heroicons/react/24/outline";
import { ServiceHeroCard } from "@/features/catalog/components/common/ServiceHeroCard";
import { FeatureCard } from "@/features/catalog/components/common/FeatureCard";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
export function CatalogHomeView() {
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
return (
<PageLayout
@ -78,7 +78,7 @@ export function CatalogHomeView() {
"Multiple access modes",
"Professional installation",
]}
href={`${shopBasePath}/internet`}
href={`${servicesBasePath}/internet`}
color="blue"
/>
<ServiceHeroCard
@ -91,7 +91,7 @@ export function CatalogHomeView() {
"Family discounts",
"Multiple data options",
]}
href={`${shopBasePath}/sim`}
href={`${servicesBasePath}/sim`}
color="green"
/>
<ServiceHeroCard
@ -104,7 +104,7 @@ export function CatalogHomeView() {
"Business & personal",
"24/7 connectivity",
]}
href={`${shopBasePath}/vpn`}
href={`${servicesBasePath}/vpn`}
color="purple"
/>
</div>

View File

@ -1,15 +1,15 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { PageLayout } from "@/components/templates/PageLayout";
import {
WifiIcon,
ServerIcon,
HomeIcon,
BuildingOfficeIcon,
CheckCircleIcon,
BoltIcon,
ClockIcon,
ExclamationTriangleIcon,
MapPinIcon,
} from "@heroicons/react/24/outline";
import { useInternetCatalog } from "@/features/catalog/hooks";
import { useActiveSubscriptions } from "@/features/subscriptions/hooks/useSubscriptions";
@ -22,13 +22,13 @@ import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { Button } from "@/components/atoms/button";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { CatalogHero } from "@/features/catalog/components/base/CatalogHero";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import { InternetImportantNotes } from "@/features/catalog/components/internet/InternetImportantNotes";
import {
InternetOfferingCard,
type TierInfo,
} from "@/features/catalog/components/internet/InternetOfferingCard";
import { PublicInternetPlansContent } from "@/features/catalog/views/PublicInternetPlans";
import { PlanComparisonGuide } from "@/features/catalog/components/internet/PlanComparisonGuide";
import {
useInternetEligibility,
@ -48,7 +48,6 @@ interface OfferingConfig {
iconType: "home" | "apartment";
isPremium: boolean;
displayOrder: number;
/** If true, this is an alternative speed option (e.g., 1G when 10G is available) */
isAlternative?: boolean;
alternativeNote?: string;
}
@ -81,20 +80,15 @@ const OFFERING_CONFIGS: Record<string, Omit<OfferingConfig, "offeringType">> = {
"Apartment 100M": {
title: "Apartment 100Mbps",
speedBadge: "100 Mbps",
description:
"Standard speed via VDSL or LAN for apartment buildings with shared infrastructure.",
description: "Standard speed via VDSL or LAN for apartment buildings.",
iconType: "apartment",
isPremium: false,
displayOrder: 2,
},
};
/**
* Get tier info from plans
*/
function getTierInfo(plans: InternetPlanCatalogItem[], offeringType: string): TierInfo[] {
const filtered = plans.filter(p => p.internetOfferingType === offeringType);
const tierOrder: ("Silver" | "Gold" | "Platinum")[] = ["Silver", "Gold", "Platinum"];
const tierDescriptions: Record<
@ -103,42 +97,34 @@ function getTierInfo(plans: InternetPlanCatalogItem[], offeringType: string): Ti
> = {
Silver: {
description: "Essential setup—bring your own router",
features: [
"NTT modem + ISP connection",
"IPoE or PPPoE protocols",
"Self-configuration required",
],
features: ["NTT modem + ISP connection", "IPoE or PPPoE protocols", "Self-configuration"],
},
Gold: {
description: "All-inclusive with router rental",
features: [
"Everything in Silver, plus:",
"Everything in Silver",
"WiFi router included",
"Auto-configured within 24hrs",
"Range extender option (+¥500/mo)",
"Auto-configured",
"Range extender option",
],
},
Platinum: {
description: "Tailored setup for larger residences",
description: "Tailored setup for larger homes",
features: [
"Netgear INSIGHT mesh routers",
"Cloud-managed WiFi network",
"Remote support & auto-updates",
"Custom setup for your space",
"Cloud-managed WiFi",
"Remote support",
"Custom setup",
],
pricingNote: "+ equipment fees based on your home",
pricingNote: "+ equipment fees",
},
};
const result: TierInfo[] = [];
for (const tier of tierOrder) {
const plan = filtered.find(p => p.internetPlanTier?.toLowerCase() === tier.toLowerCase());
if (!plan) continue;
const config = tierDescriptions[tier];
result.push({
tier,
monthlyPrice: plan.monthlyPrice ?? 0,
@ -148,22 +134,14 @@ function getTierInfo(plans: InternetPlanCatalogItem[], offeringType: string): Ti
pricingNote: config.pricingNote,
});
}
return result;
}
/**
* Get the setup fee from installations
*/
function getSetupFee(installations: InternetInstallationCatalogItem[]): number {
const basic = installations.find(i => i.sku?.toLowerCase().includes("basic"));
return basic?.oneTimePrice ?? 22800;
}
/**
* Determine which offering types are available based on eligibility
* Returns an array of offering configs, potentially with alternatives
*/
function getAvailableOfferings(
eligibility: string | null,
plans: InternetPlanCatalogItem[]
@ -173,66 +151,40 @@ function getAvailableOfferings(
const results: OfferingConfig[] = [];
const eligibilityLower = eligibility.toLowerCase();
// Check if this is a "Home 10G" eligibility - they can also choose 1G
if (eligibilityLower.includes("home 10g")) {
const config10g = OFFERING_CONFIGS["Home 10G"];
const config1g = OFFERING_CONFIGS["Home 1G"];
// Add 10G as primary
if (config10g && plans.some(p => p.internetOfferingType === "Home 10G")) {
results.push({
offeringType: "Home 10G",
...config10g,
});
results.push({ offeringType: "Home 10G", ...config10g });
}
// Add 1G as alternative (lower cost option)
if (config1g && plans.some(p => p.internetOfferingType === "Home 1G")) {
results.push({
offeringType: "Home 1G",
...config1g,
isAlternative: true,
alternativeNote: "Choose this if you prefer a lower monthly cost",
alternativeNote: "Lower monthly cost option",
});
}
}
// Home 1G only - cannot upgrade to 10G
else if (eligibilityLower.includes("home 1g")) {
} else if (eligibilityLower.includes("home 1g")) {
const config = OFFERING_CONFIGS["Home 1G"];
if (config && plans.some(p => p.internetOfferingType === "Home 1G")) {
results.push({
offeringType: "Home 1G",
...config,
});
results.push({ offeringType: "Home 1G", ...config });
}
}
// Apartment 1G
else if (eligibilityLower.includes("apartment 1g")) {
} else if (eligibilityLower.includes("apartment 1g")) {
const config = OFFERING_CONFIGS["Apartment 1G"];
if (config && plans.some(p => p.internetOfferingType === "Apartment 1G")) {
results.push({
offeringType: "Apartment 1G",
...config,
});
results.push({ offeringType: "Apartment 1G", ...config });
}
}
// Apartment 100M (VDSL/LAN)
else if (eligibilityLower.includes("apartment 100m")) {
} else if (eligibilityLower.includes("apartment 100m")) {
const config = OFFERING_CONFIGS["Apartment 100M"];
if (config && plans.some(p => p.internetOfferingType === "Apartment 100M")) {
results.push({
offeringType: "Apartment 100M",
...config,
});
results.push({ offeringType: "Apartment 100M", ...config });
}
}
return results.sort((a, b) => a.displayOrder - b.displayOrder);
}
/**
* Format eligibility for display
*/
function formatEligibilityDisplay(eligibility: string): {
residenceType: "home" | "apartment";
speed: string;
@ -262,7 +214,7 @@ function formatEligibilityDisplay(eligibility: string): {
return {
residenceType: "apartment",
speed: "1 Gbps",
label: "Apartment/Mansion (1Gbps)",
label: "Apartment/Mansion (1Gbps FTTH)",
description: "Your building has fiber-to-the-unit infrastructure supporting 1Gbps speeds.",
};
}
@ -275,7 +227,6 @@ function formatEligibilityDisplay(eligibility: string): {
};
}
// Default fallback
return {
residenceType: "home",
speed: eligibility,
@ -284,8 +235,71 @@ function formatEligibilityDisplay(eligibility: string): {
};
}
// Status badge component
function EligibilityStatusBadge({
status,
speed,
}: {
status: "eligible" | "pending" | "not_requested" | "ineligible";
speed?: string;
}) {
const configs = {
eligible: {
icon: CheckCircleIcon,
bg: "bg-success-soft",
border: "border-success/30",
text: "text-success",
label: "Service Available",
},
pending: {
icon: ClockIcon,
bg: "bg-info-soft",
border: "border-info/30",
text: "text-info",
label: "Review in Progress",
},
not_requested: {
icon: MapPinIcon,
bg: "bg-muted",
border: "border-border",
text: "text-muted-foreground",
label: "Verification Required",
},
ineligible: {
icon: ExclamationTriangleIcon,
bg: "bg-warning/10",
border: "border-warning/30",
text: "text-warning",
label: "Not Available",
},
};
const config = configs[status];
const Icon = config.icon;
return (
<div
className={cn(
"inline-flex items-center gap-2 px-4 py-2 rounded-full border",
config.bg,
config.border
)}
>
<Icon className={cn("h-4 w-4", config.text)} />
<span className={cn("font-semibold text-sm", config.text)}>{config.label}</span>
{status === "eligible" && speed && (
<>
<span className="text-muted-foreground">·</span>
<span className="text-sm text-foreground font-medium">Up to {speed}</span>
</>
)}
</div>
);
}
export function InternetPlansContainer() {
const shopBasePath = useShopBasePath();
const router = useRouter();
const servicesBasePath = useServicesBasePath();
const searchParams = useSearchParams();
const { user } = useAuthSession();
const { data, isLoading, error } = useInternetCatalog();
@ -327,9 +341,6 @@ export function InternetPlansContainer() {
const isNotRequested = eligibilityStatus === "not_requested";
const isIneligible = eligibilityStatus === "ineligible";
// While loading eligibility, we assume locked to prevent showing unfiltered catalog for a split second
const orderingLocked =
eligibilityLoading || isPending || isNotRequested || isIneligible || !eligibilityStatus;
const hasServiceAddress = Boolean(
user?.address?.address1 &&
user?.address?.city &&
@ -357,51 +368,62 @@ export function InternetPlansContainer() {
const setupFee = useMemo(() => getSetupFee(installations), [installations]);
// Get available offerings based on eligibility
const availableOfferings = useMemo(() => {
if (!eligibility) return [];
return getAvailableOfferings(eligibility, plans);
}, [eligibility, plans]);
// Format eligibility for display
const eligibilityDisplay = useMemo(() => {
if (!eligibility) return null;
return formatEligibilityDisplay(eligibility);
}, [eligibility]);
// Build offering cards data
const offeringCards = useMemo(() => {
return availableOfferings
.map(config => {
const tiers = getTierInfo(plans, config.offeringType);
const startingPrice = tiers.length > 0 ? Math.min(...tiers.map(t => t.monthlyPrice)) : 0;
return {
...config,
tiers,
startingPrice,
setupFee,
ctaPath: `/shop/internet/configure`,
ctaPath: `${servicesBasePath}/internet/configure`,
};
})
.filter(card => card.tiers.length > 0);
}, [availableOfferings, plans, setupFee]);
}, [availableOfferings, plans, setupFee, servicesBasePath]);
// Logic to handle check availability click
const handleCheckAvailability = async (e?: React.MouseEvent) => {
if (e) e.preventDefault();
if (!hasServiceAddress) {
// Should redirect to address page if not handled by parent UI
router.push("/account/settings");
return;
}
// Trigger eligibility check
const confirmed =
typeof window === "undefined" ||
window.confirm(`Request availability check for:\n\n${addressLabel}`);
if (!confirmed) return;
eligibilityRequest.mutate({ address: user?.address ?? undefined });
};
// Auto eligibility request effect
useEffect(() => {
if (!autoEligibilityRequest) return;
if (autoRequestStatus !== "idle") return;
if (eligibilityLoading) return;
if (!isNotRequested) {
if (typeof window !== "undefined") {
window.history.replaceState(null, "", `${shopBasePath}/internet`);
}
router.replace(`${servicesBasePath}/internet`);
return;
}
if (!hasServiceAddress) {
setAutoRequestStatus("missing_address");
if (typeof window !== "undefined") {
window.history.replaceState(null, "", `${shopBasePath}/internet`);
}
router.replace(`${servicesBasePath}/internet`);
return;
}
@ -418,13 +440,10 @@ export function InternetPlansContainer() {
setAutoRequestId(result.requestId ?? null);
setAutoRequestStatus("submitted");
await refetchEligibility();
} catch (err) {
void err;
} catch {
setAutoRequestStatus("failed");
} finally {
if (typeof window !== "undefined") {
window.history.replaceState(null, "", `${shopBasePath}/internet`);
}
router.replace(`${servicesBasePath}/internet`);
}
};
@ -438,10 +457,12 @@ export function InternetPlansContainer() {
submitEligibilityRequest,
hasServiceAddress,
isNotRequested,
shopBasePath,
servicesBasePath,
user?.address,
router,
]);
// Loading state
if (isLoading || error) {
return (
<PageLayout
@ -451,15 +472,13 @@ export function InternetPlansContainer() {
>
<AsyncBlock isLoading={false} error={error}>
<div className="max-w-4xl mx-auto px-4">
<CatalogBackLink href={shopBasePath} label="Back to Services" className="mb-4" />
<CatalogBackLink href={servicesBasePath} label="Back to Services" className="mb-4" />
<div className="text-center mb-12">
<Skeleton className="h-10 w-96 mx-auto mb-4" />
<Skeleton className="h-4 w-[32rem] max-w-full mx-auto" />
</div>
<div className="space-y-4">
{Array.from({ length: 2 }).map((_, i) => (
{[1, 2].map(i => (
<div
key={i}
className="bg-card rounded-xl border border-border p-6 shadow-[var(--cp-shadow-1)]"
@ -481,272 +500,164 @@ export function InternetPlansContainer() {
);
}
// Determine current status for the badge
const currentStatus = isEligible
? "eligible"
: isPending
? "pending"
: isIneligible
? "ineligible"
: "not_requested";
// Case 1: Unverified / Not Requested - Show Public Content exactly
if (isNotRequested && autoRequestStatus !== "submitting" && autoRequestStatus !== "submitted") {
return (
<PageLayout
title=""
description=""
// Hide standard header by passing empty props or managing children
// PageLayout might still render a container.
>
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
{/* Already has internet warning */}
{hasActiveInternet && (
<AlertBanner variant="warning" title="Active subscription found" className="mb-6">
You already have an internet subscription. For additional residences, please{" "}
<a href="/account/support/new" className="underline font-medium">
contact support
</a>
.
</AlertBanner>
)}
{/* Auto-request status alerts - only show for errors/success */}
{autoRequestStatus === "failed" && (
<AlertBanner variant="warning" title="Request failed" className="mb-6">
Please try again below or contact support.
</AlertBanner>
)}
{autoRequestStatus === "missing_address" && (
<AlertBanner variant="warning" title="Address required" className="mb-6">
<div className="flex items-center justify-between gap-4">
<span>Add your service address to request availability verification.</span>
<Button as="a" href="/account/settings" size="sm">
Add address
</Button>
</div>
</AlertBanner>
)}
<PublicInternetPlansContent
onCtaClick={handleCheckAvailability}
ctaLabel={hasServiceAddress ? "Check Availability" : "Add Service Address"}
/>
</div>
</PageLayout>
);
}
// Case 2: Standard Portal View (Pending, Eligible, Ineligible, Loading)
return (
<PageLayout
title="Internet Plans"
description="High-speed internet services for your home or business"
description="High-speed internet services for your home"
icon={<WifiIcon className="h-6 w-6" />}
>
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
<CatalogBackLink href={shopBasePath} label="Back to Services" className="mb-4" />
<CatalogBackLink href={servicesBasePath} label="Back to Services" className="mb-6" />
<CatalogHero
title="Your Internet Options"
description="Plans tailored to your residence and available infrastructure."
>
{eligibilityLoading ? (
<div className="flex flex-col items-center gap-2">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-border bg-muted shadow-[var(--cp-shadow-1)]">
<span className="font-semibold text-foreground">Checking availability</span>
</div>
<p className="text-sm text-muted-foreground text-center max-w-md">
We're verifying what service is available at your residence.
</p>
</div>
) : autoRequestStatus === "submitting" ? (
<div className="flex flex-col items-center gap-2">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-border bg-muted shadow-[var(--cp-shadow-1)]">
<span className="font-semibold text-foreground">
Submitting availability request
</span>
</div>
<p className="text-sm text-muted-foreground text-center max-w-md">
We're sending your request now.
</p>
</div>
) : autoRequestStatus === "submitted" ? (
<div className="flex flex-col items-center gap-2">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-info/25 bg-info-soft text-info shadow-[var(--cp-shadow-1)]">
<span className="font-semibold">Availability review in progress</span>
</div>
<p className="text-sm text-muted-foreground text-center max-w-md">
We've received your request and will notify you when the review is complete.
</p>
</div>
) : isNotRequested ? (
<div className="flex flex-col items-center gap-2">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-border bg-muted shadow-[var(--cp-shadow-1)]">
<span className="font-semibold text-foreground">Availability review required</span>
</div>
<p className="text-sm text-muted-foreground text-center max-w-md">
Request an eligibility review to unlock personalized internet plans for your
residence.
</p>
</div>
) : isPending ? (
<div className="flex flex-col items-center gap-2">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-info/25 bg-info-soft text-info shadow-[var(--cp-shadow-1)]">
<span className="font-semibold">Availability review in progress</span>
</div>
<p className="text-sm text-muted-foreground text-center max-w-md">
We're reviewing service availability for your address. Once confirmed, we'll unlock
your personalized internet plans.
</p>
</div>
) : isIneligible ? (
<div className="flex flex-col items-center gap-2">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-warning/25 bg-warning/10 text-warning shadow-[var(--cp-shadow-1)]">
<span className="font-semibold">Not available for this address</span>
</div>
<p className="text-sm text-muted-foreground text-center max-w-md">
Our team reviewed your address and determined service isn't available right now.
</p>
</div>
) : null}
</CatalogHero>
{/* Hero section - compact (for portal view) */}
<div className="text-center mb-8">
<h1 className="text-2xl md:text-3xl font-bold text-foreground mb-2">
Your Internet Options
</h1>
<p className="text-muted-foreground mb-4">
Plans tailored to your residence and available infrastructure
</p>
{/* Auto-request status alerts */}
{autoRequestStatus === "submitting" && (
<AlertBanner variant="info" title="Submitting availability request" className="mb-8">
We're sending your request now. You'll see updated eligibility once the review begins.
{/* Status badge */}
{!eligibilityLoading && autoRequestStatus !== "submitting" && (
<EligibilityStatusBadge status={currentStatus} speed={eligibilityDisplay?.speed} />
)}
{/* Loading states */}
{(eligibilityLoading || autoRequestStatus === "submitting") && (
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-border bg-muted">
<div className="h-4 w-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
<span className="font-medium text-sm text-foreground">
{autoRequestStatus === "submitting"
? "Submitting request..."
: "Checking status..."}
</span>
</div>
)}
</div>
{/* Already has internet warning */}
{hasActiveInternet && (
<AlertBanner variant="warning" title="Active subscription found" className="mb-6">
You already have an internet subscription. For additional residences, please{" "}
<a href="/account/support/new" className="underline font-medium">
contact support
</a>
.
</AlertBanner>
)}
{/* Auto-request status alerts - only show for errors/success */}
{autoRequestStatus === "submitted" && (
<AlertBanner variant="success" title="Request received" className="mb-8">
We've received your availability request. Our team will investigate and notify you when
the review is complete.
<AlertBanner variant="success" title="Request submitted" className="mb-6">
We'll verify your address and notify you when complete.
{autoRequestId && (
<div className="text-xs text-muted-foreground mt-2">
Request ID: <span className="font-mono">{autoRequestId}</span>
</div>
<span className="text-xs text-muted-foreground ml-2">ID: {autoRequestId}</span>
)}
</AlertBanner>
)}
{autoRequestStatus === "failed" && (
<AlertBanner variant="warning" title="We couldn't submit your request" className="mb-8">
Please try again below or contact support if this keeps happening.
<AlertBanner variant="warning" title="Request failed" className="mb-6">
Please try again below or contact support.
</AlertBanner>
)}
{autoRequestStatus === "missing_address" && (
<AlertBanner variant="warning" title="We need your address to continue" className="mb-8">
Add your service address so we can submit the availability request.
</AlertBanner>
)}
{/* Eligibility request section */}
{isNotRequested &&
autoRequestStatus !== "submitting" &&
autoRequestStatus !== "submitted" && (
<AlertBanner variant="info" title="Request an eligibility review" className="mb-8">
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<p className="text-sm text-foreground/80">
Our team will verify NTT serviceability and update your eligible offerings. We'll
notify you when review is complete.
</p>
{hasServiceAddress ? (
<Button
type="button"
size="sm"
disabled={eligibilityRequest.isPending}
isLoading={eligibilityRequest.isPending}
loadingText="Requesting…"
onClick={() =>
void (async () => {
const confirmed =
typeof window === "undefined" ||
window.confirm(
`Request an eligibility review for this address?\n\n${addressLabel}`
);
if (!confirmed) return;
eligibilityRequest.mutate({ address: user?.address ?? undefined });
})()
}
className="sm:ml-auto whitespace-nowrap"
>
Request review now
</Button>
) : (
<Button
as="a"
href="/account/settings"
size="sm"
className="sm:ml-auto whitespace-nowrap"
>
Add address to continue
</Button>
)}
</div>
</AlertBanner>
)}
{isPending && (
<AlertBanner variant="info" title="Review in progress" className="mb-8">
<div className="space-y-2">
<p className="text-sm text-foreground/80">
We'll notify you when review is complete.
</p>
{requestedAt ? (
<p className="text-xs text-muted-foreground">
Requested: {new Date(requestedAt).toLocaleString()}
</p>
) : null}
</div>
</AlertBanner>
)}
{isIneligible && (
<AlertBanner variant="warning" title="Service not available" className="mb-8">
<div className="space-y-2">
{rejectionNotes ? (
<p className="text-sm text-foreground/80">{rejectionNotes}</p>
) : (
<p className="text-sm text-foreground/80">
If you believe this is incorrect, contact support and we'll take another look.
</p>
)}
<Button as="a" href="/account/support/new" size="sm">
Contact support
<AlertBanner variant="warning" title="Address required" className="mb-6">
<div className="flex items-center justify-between gap-4">
<span>Add your service address to request availability verification.</span>
<Button as="a" href="/account/settings" size="sm">
Add address
</Button>
</div>
</AlertBanner>
)}
{hasActiveInternet && (
<AlertBanner
variant="warning"
title="You already have an Internet subscription"
className="mb-8"
>
<p>
You already have an Internet subscription with us. If you want another subscription
for a different residence, please{" "}
<a
href="/account/support/new"
className="underline text-primary hover:text-primary-hover font-medium transition-colors"
>
contact us
</a>
.
</p>
</AlertBanner>
)}
{/* ELIGIBLE - Show personalized plans */}
{/* ELIGIBLE STATE - Clean & Personalized */}
{isEligible && eligibilityDisplay && offeringCards.length > 0 && (
<>
{/* Eligibility confirmation box */}
<div className="bg-success-soft border border-success/25 rounded-xl p-5 mb-8">
<div className="flex items-start gap-4">
<div
className={cn(
"flex h-12 w-12 items-center justify-center rounded-xl border flex-shrink-0",
eligibilityDisplay.residenceType === "home"
? "bg-info-soft text-info border-info/25"
: "bg-success-soft text-success border-success/25"
)}
>
{eligibilityDisplay.residenceType === "home" ? (
<HomeIcon className="h-6 w-6" />
) : (
<BuildingOfficeIcon className="h-6 w-6" />
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<CheckCircleIcon className="h-5 w-5 text-success" />
<h3 className="font-semibold text-foreground">Service Available</h3>
</div>
<p className="text-sm font-medium text-foreground mb-1">
{eligibilityDisplay.label}
</p>
<p className="text-sm text-muted-foreground">{eligibilityDisplay.description}</p>
</div>
<div className="hidden sm:flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20">
<BoltIcon className="h-4 w-4 text-primary" />
<span className="text-sm font-semibold text-primary">
Up to {eligibilityDisplay.speed}
</span>
</div>
</div>
</div>
{/* Plan comparison guide */}
<div className="mb-8">
<div className="mb-6">
<PlanComparisonGuide />
</div>
{/* Available speed options */}
{/* Speed options header (only if multiple) */}
{offeringCards.length > 1 && (
<div className="mb-6">
<h2 className="text-lg font-semibold text-foreground mb-2">Choose your speed</h2>
<div className="mb-4">
<h2 className="text-lg font-semibold text-foreground">Choose your speed</h2>
<p className="text-sm text-muted-foreground">
Your address supports multiple speed options. Pick the one that fits your needs
and budget.
Your address supports multiple options
</p>
</div>
)}
{/* Offering cards */}
<div className="space-y-4 mb-8">
{offeringCards.map((card, index) => (
<div className="space-y-4 mb-6">
{offeringCards.map(card => (
<div key={card.offeringType}>
{card.isAlternative && (
<div className="flex items-center gap-2 mb-3">
<div className="h-px flex-1 bg-border" />
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
<span className="text-xs font-medium text-muted-foreground">
Alternative option
</span>
<div className="h-px flex-1 bg-border" />
@ -763,106 +674,77 @@ export function InternetPlansContainer() {
tiers={card.tiers}
ctaPath={card.ctaPath}
isPremium={card.isPremium}
defaultExpanded={index === 0}
defaultExpanded={false}
disabled={hasActiveInternet}
disabledReason={
hasActiveInternet
? "Already subscribed — contact us to add another residence"
: undefined
hasActiveInternet ? "Contact support for additional lines" : undefined
}
/>
</div>
))}
</div>
{/* Important notes */}
{/* Important notes - collapsed by default */}
<InternetImportantNotes />
<CatalogBackLink
href={shopBasePath}
href={servicesBasePath}
label="Back to Services"
align="center"
className="mt-12 mb-0"
className="mt-10"
/>
</>
)}
{/* NOT ELIGIBLE YET - Show locked state */}
{orderingLocked && !isEligible && plans.length > 0 && (
{/* PENDING STATE - Clean Status View */}
{isPending && (
<>
<AlertBanner
variant="info"
title={
isIneligible
? "Ordering unavailable"
: isNotRequested
? "Eligibility review required"
: "Availability review in progress"
}
className="mb-8"
elevated
>
<p className="text-sm text-foreground/80">
{isIneligible
? "Service is not available for your address."
: isNotRequested
? "Request an eligibility review to unlock ordering for your residence."
: "You can browse plan options below, but ordering stays locked until we confirm service availability for your residence."}
<div className="bg-info-soft/30 border border-info/20 rounded-xl p-8 mb-6 text-center max-w-2xl mx-auto">
<ClockIcon className="h-16 w-16 text-info mx-auto mb-6" />
<h2 className="text-2xl font-semibold text-foreground mb-3">
Verification in Progress
</h2>
<p className="text-base text-muted-foreground mb-4 leading-relaxed">
We're currently verifying NTT service availability at your registered address.
<br />
This manual check ensures we offer you the correct fiber connection type.
</p>
</AlertBanner>
{/* Show plan comparison guide even when locked */}
<div className="mb-8">
<PlanComparisonGuide />
</div>
<div className="opacity-60 pointer-events-none">
<p className="text-center text-sm text-muted-foreground mb-6">
Preview of available plans (ordering locked)
</p>
{/* Show a simplified preview */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.entries(OFFERING_CONFIGS)
.slice(0, 4)
.map(([key, config]) => (
<div
key={key}
className="bg-card rounded-xl border border-border p-5 shadow-[var(--cp-shadow-1)]"
>
<div className="flex items-center gap-3">
<div
className={cn(
"flex h-10 w-10 items-center justify-center rounded-xl border",
config.iconType === "home"
? "bg-info-soft text-info border-info/25"
: "bg-success-soft text-success border-success/25"
)}
>
{config.iconType === "home" ? (
<HomeIcon className="h-5 w-5" />
) : (
<BuildingOfficeIcon className="h-5 w-5" />
)}
</div>
<div>
<h3 className="font-semibold text-foreground">{config.title}</h3>
<p className="text-xs text-muted-foreground">{config.speedBadge}</p>
</div>
</div>
</div>
))}
<div className="inline-flex flex-col items-center p-4 bg-background rounded-lg border border-border">
<span className="text-sm font-medium text-foreground mb-1">Estimated time</span>
<span className="text-sm text-muted-foreground">1-2 business days</span>
</div>
{requestedAt && (
<p className="text-xs text-muted-foreground mt-6">
Request submitted: {new Date(requestedAt).toLocaleDateString()}
</p>
)}
</div>
<CatalogBackLink
href={shopBasePath}
label="Back to Services"
align="center"
className="mt-12 mb-0"
/>
<div className="text-center">
<Button as="a" href={servicesBasePath} variant="outline">
Back to Catalog
</Button>
</div>
</>
)}
{/* INELIGIBLE STATE */}
{isIneligible && (
<div className="bg-warning/5 border border-warning/20 rounded-xl p-6 text-center">
<ExclamationTriangleIcon className="h-12 w-12 text-warning mx-auto mb-4" />
<h2 className="text-lg font-semibold text-foreground mb-2">Service not available</h2>
<p className="text-sm text-muted-foreground mb-4 max-w-md mx-auto">
{rejectionNotes ||
"Our review determined that NTT fiber service isn't available at your address."}
</p>
<Button as="a" href="/account/support/new" variant="outline">
Contact support
</Button>
</div>
)}
{/* No plans available */}
{plans.length === 0 && !isLoading && (
<div className="text-center py-16">
@ -870,14 +752,9 @@ export function InternetPlansContainer() {
<ServerIcon className="h-16 w-16 text-muted-foreground mx-auto mb-6" />
<h3 className="text-xl font-semibold text-foreground mb-2">No Plans Available</h3>
<p className="text-muted-foreground mb-8">
We couldn&apos;t find any internet plans available at this time.
We couldn&apos;t find any internet plans at this time.
</p>
<CatalogBackLink
href={shopBasePath}
label="Back to Services"
align="center"
className="mt-0 mb-0"
/>
<CatalogBackLink href={servicesBasePath} label="Back to Services" align="center" />
</div>
</div>
)}

View File

@ -1,149 +1,165 @@
"use client";
import React from "react";
import Link from "next/link";
import {
Squares2X2Icon,
ServerIcon,
DevicePhoneMobileIcon,
ShieldCheckIcon,
WifiIcon,
GlobeAltIcon,
ClockIcon,
BoltIcon,
CheckCircleIcon,
ArrowRightIcon,
} from "@heroicons/react/24/outline";
import { ServiceHeroCard } from "@/features/catalog/components/common/ServiceHeroCard";
import { FeatureCard } from "@/features/catalog/components/common/FeatureCard";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
/**
* Public Catalog Home View
*
* Similar to CatalogHomeView but designed for unauthenticated users.
* Uses public catalog paths and doesn't require PageLayout with auth.
* Purpose: Browse and compare services
* Contains:
* - Simple hero
* - Detailed service cards with features
* - Help link to contact
*
* Note: Value props are on the homepage, not repeated here
*/
export function PublicCatalogHomeView() {
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
return (
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<div className="inline-flex items-center gap-2 bg-muted text-foreground px-4 py-2 rounded-full text-sm font-medium mb-4">
<Squares2X2Icon className="h-4 w-4 text-primary" />
Services Catalog
</div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground leading-tight">
Choose your connectivity solution
<div className="max-w-6xl mx-auto space-y-12">
{/* Hero Section - Simple and focused */}
<div className="text-center">
<h1 className="text-3xl sm:text-4xl font-bold text-foreground tracking-tight mb-3">
Browse Our Services
</h1>
<p className="text-sm sm:text-base text-muted-foreground mt-2 max-w-3xl leading-relaxed">
Explore our internet, mobile, and VPN services. Browse plans and pricing, then create an
account when you&apos;re ready to order.
<p className="text-lg text-muted-foreground max-w-xl mx-auto">
High-speed internet, mobile plans, and VPN services with full English support.
</p>
</div>
{/* Service-specific ordering info */}
<div className="bg-muted/40 border border-border rounded-2xl p-6 mb-10">
<h2 className="text-sm font-semibold text-foreground mb-4">What to expect when ordering</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-500/10 text-blue-500 border border-blue-500/20 flex-shrink-0">
<ClockIcon className="h-5 w-5" />
{/* Service Cards */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Internet Card */}
<Link
href={`${servicesBasePath}/internet`}
className="group relative overflow-hidden rounded-2xl border border-border bg-card p-8 hover:border-blue-500/50 transition-all duration-300"
>
<div className="absolute top-0 right-0 -translate-y-1/2 translate-x-1/4 w-32 h-32 bg-blue-500/10 rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="relative">
<div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-blue-500/20 to-blue-600/10 border border-blue-500/20 flex items-center justify-center mb-6 text-blue-500 group-hover:scale-110 transition-transform duration-300">
<ServerIcon className="h-7 w-7" />
</div>
<div>
<div className="text-sm font-semibold text-foreground">Internet</div>
<div className="text-xs text-muted-foreground">
Requires address verification (1-2 business days). We&apos;ll email you when plans
are ready.
</div>
<h2 className="text-2xl font-bold text-foreground group-hover:text-blue-500 transition-colors mb-3">
Internet
</h2>
<p className="text-muted-foreground mb-6 leading-relaxed">
NTT fiber with speeds up to 10Gbps and professional installation support.
</p>
<ul className="space-y-2 mb-6">
{[
"Up to 10Gbps speeds",
"Fiber optic network",
"WiFi router options",
"English support",
].map((feature, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircleIcon className="h-4 w-4 text-blue-500 flex-shrink-0" />
{feature}
</li>
))}
</ul>
<div className="flex items-center gap-2 text-sm font-semibold text-blue-500 group-hover:gap-3 transition-all">
View Plans
<ArrowRightIcon className="h-4 w-4" />
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-green-500/10 text-green-500 border border-green-500/20 flex-shrink-0">
<BoltIcon className="h-5 w-5" />
</Link>
{/* SIM Card */}
<Link
href={`${servicesBasePath}/sim`}
className="group relative overflow-hidden rounded-2xl border border-border bg-card p-8 hover:border-green-500/50 transition-all duration-300"
>
<div className="absolute top-0 right-0 -translate-y-1/2 translate-x-1/4 w-32 h-32 bg-green-500/10 rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="absolute top-4 right-4">
<span className="inline-flex items-center rounded-full bg-green-500/10 border border-green-500/20 px-2.5 py-0.5 text-xs font-semibold text-green-600">
First Month Free
</span>
</div>
<div className="relative">
<div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-green-500/20 to-green-600/10 border border-green-500/20 flex items-center justify-center mb-6 text-green-500 group-hover:scale-110 transition-transform duration-300">
<DevicePhoneMobileIcon className="h-7 w-7" />
</div>
<div>
<div className="text-sm font-semibold text-foreground">SIM & eSIM</div>
<div className="text-xs text-muted-foreground">
Order immediately after signup. Physical SIM ships next business day.
</div>
<h2 className="text-2xl font-bold text-foreground group-hover:text-green-500 transition-colors mb-3">
SIM & eSIM
</h2>
<p className="text-muted-foreground mb-6 leading-relaxed">
Data, voice & SMS on NTT Docomo&apos;s nationwide network.
</p>
<ul className="space-y-2 mb-6">
{["Physical SIM & eSIM", "Flexible data plans", "First month free"].map(
(feature, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircleIcon className="h-4 w-4 text-green-500 flex-shrink-0" />
{feature}
</li>
)
)}
</ul>
<div className="flex items-center gap-2 text-sm font-semibold text-green-500 group-hover:gap-3 transition-all">
View Plans
<ArrowRightIcon className="h-4 w-4" />
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-purple-500/10 text-purple-500 border border-purple-500/20 flex-shrink-0">
<BoltIcon className="h-5 w-5" />
</Link>
{/* VPN Card */}
<Link
href={`${servicesBasePath}/vpn`}
className="group relative overflow-hidden rounded-2xl border border-border bg-card p-8 hover:border-purple-500/50 transition-all duration-300"
>
<div className="absolute top-0 right-0 -translate-y-1/2 translate-x-1/4 w-32 h-32 bg-purple-500/10 rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="relative">
<div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-purple-500/20 to-purple-600/10 border border-purple-500/20 flex items-center justify-center mb-6 text-purple-500 group-hover:scale-110 transition-transform duration-300">
<ShieldCheckIcon className="h-7 w-7" />
</div>
<div>
<div className="text-sm font-semibold text-foreground">VPN</div>
<div className="text-xs text-muted-foreground">
Order immediately after signup. Router shipped upon order confirmation.
</div>
<h2 className="text-2xl font-bold text-foreground group-hover:text-purple-500 transition-colors mb-3">
VPN
</h2>
<p className="text-muted-foreground mb-6 leading-relaxed">
Access US/UK content with a pre-configured router.
</p>
<ul className="space-y-2 mb-6">
{[
"Router rental included",
"San Francisco & London",
"Easy plug & play setup",
"Stream overseas content",
].map((feature, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircleIcon className="h-4 w-4 text-purple-500 flex-shrink-0" />
{feature}
</li>
))}
</ul>
<div className="flex items-center gap-2 text-sm font-semibold text-purple-500 group-hover:gap-3 transition-all">
View Plans
<ArrowRightIcon className="h-4 w-4" />
</div>
</div>
</div>
</Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-10">
<ServiceHeroCard
title="Internet Service"
description="Ultra-high-speed fiber internet with speeds up to 10Gbps."
icon={<ServerIcon className="h-10 w-10" />}
features={[
"Up to 10Gbps speeds",
"Fiber optic technology",
"Multiple access modes",
"Professional installation",
]}
href={`${shopBasePath}/internet`}
color="blue"
/>
<ServiceHeroCard
title="SIM & eSIM"
description="Data, SMS, and voice plans with both physical SIM and eSIM options."
icon={<DevicePhoneMobileIcon className="h-10 w-10" />}
features={[
"Physical SIM & eSIM",
"Data + SMS + Voice plans",
"Family discounts",
"Multiple data options",
]}
href={`${shopBasePath}/sim`}
color="green"
/>
<ServiceHeroCard
title="VPN Service"
description="Secure remote access solutions for business and personal use."
icon={<ShieldCheckIcon className="h-10 w-10" />}
features={[
"Secure encryption",
"Multiple locations",
"Business & personal",
"24/7 connectivity",
]}
href={`${shopBasePath}/vpn`}
color="purple"
/>
</div>
<div className="bg-card text-card-foreground rounded-2xl p-8 border border-border shadow-[var(--cp-shadow-1)]">
<div className="mb-6">
<h3 className="text-lg sm:text-xl font-semibold text-foreground mb-2">
Why choose our services?
</h3>
<p className="text-sm text-muted-foreground max-w-2xl leading-relaxed">
Reliable connectivity with transparent pricing and dedicated support.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FeatureCard
icon={<WifiIcon className="h-8 w-8 text-primary" />}
title="Quality Networks"
description="NTT fiber for internet, 5G coverage for mobile, secure VPN infrastructure"
/>
<FeatureCard
icon={<GlobeAltIcon className="h-8 w-8 text-primary" />}
title="Simple Management"
description="Manage all your services, billing, and support from one account portal"
/>
</div>
{/* Help Link - Simple */}
<div className="text-center pt-4">
<p className="text-muted-foreground mb-2">Need help choosing the right plan?</p>
<Link
href="/contact"
className="inline-flex items-center gap-2 text-primary font-semibold hover:text-primary/80 transition-colors"
>
Contact Us
<ArrowRightIcon className="h-4 w-4" />
</Link>
</div>
</div>
);

View File

@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation";
import { WifiIcon, ClockIcon, EnvelopeIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { InlineAuthSection } from "@/features/auth/components/InlineAuthSection/InlineAuthSection";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import { useInternetPlan } from "@/features/catalog/hooks";
import { CardPricing } from "@/features/catalog/components/base/CardPricing";
import { Skeleton } from "@/components/atoms/loading-skeleton";
@ -15,19 +15,19 @@ import { Skeleton } from "@/components/atoms/loading-skeleton";
* Clean signup flow - auth form is the focus, "what happens next" is secondary info.
*/
export function PublicInternetConfigureView() {
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const searchParams = useSearchParams();
const planSku = searchParams?.get("planSku");
const { plan, isLoading } = useInternetPlan(planSku || undefined);
const redirectTo = planSku
? `/account/shop/internet?autoEligibilityRequest=1&planSku=${encodeURIComponent(planSku)}`
: "/account/shop/internet?autoEligibilityRequest=1";
? `/account/services/internet?autoEligibilityRequest=1&planSku=${encodeURIComponent(planSku)}`
: "/account/services/internet?autoEligibilityRequest=1";
if (isLoading) {
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
<CatalogBackLink href={`${shopBasePath}/internet`} label="Back to plans" />
<CatalogBackLink href={`${servicesBasePath}/internet`} label="Back to plans" />
<div className="mt-8 space-y-6">
<Skeleton className="h-10 w-96 mx-auto" />
<Skeleton className="h-32 w-full" />
@ -38,7 +38,7 @@ export function PublicInternetConfigureView() {
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
<CatalogBackLink href={`${shopBasePath}/internet`} label="Back to plans" />
<CatalogBackLink href={`${servicesBasePath}/internet`} label="Back to plans" />
{/* Header */}
<div className="mt-6 mb-6 text-center">

View File

@ -6,6 +6,12 @@ import {
SparklesIcon,
ChevronDownIcon,
ChevronUpIcon,
WifiIcon,
BoltIcon,
ChatBubbleLeftRightIcon,
DocumentTextIcon,
WrenchScrewdriverIcon,
GlobeAltIcon,
} from "@heroicons/react/24/outline";
import { useInternetCatalog } from "@/features/catalog/hooks";
import type {
@ -15,13 +21,14 @@ import type {
import { Skeleton } from "@/components/atoms/loading-skeleton";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import { Button } from "@/components/atoms/button";
// Streamlined components
import { WhyChooseUsPillars } from "@/features/catalog/components/internet/WhyChooseUsPillars";
import { PublicOfferingCard } from "@/features/catalog/components/internet/PublicOfferingCard";
import type { TierInfo } from "@/features/catalog/components/internet/PublicOfferingCard";
import {
ServiceHighlights,
HighlightFeature,
} from "@/features/catalog/components/base/ServiceHighlights";
// Types
interface GroupedOffering {
@ -108,20 +115,69 @@ function FAQItem({
);
}
export interface PublicInternetPlansContentProps {
onCtaClick?: (e: React.MouseEvent) => void;
ctaPath?: string;
ctaLabel?: string;
heroTitle?: string;
heroDescription?: string;
}
/**
* Public Internet Plans page - Marketing/Conversion focused
* Clean, polished design optimized for conversion
*
* Note: Apartment types (FTTH 1G, VDSL 100M, LAN 100M) are consolidated into a single
* "Apartment" offering since they all have the same pricing. The actual connection type
* is determined by the building infrastructure during eligibility check.
* Public Internet Plans Content - Reusable component
*/
export function PublicInternetPlansView() {
export function PublicInternetPlansContent({
onCtaClick,
ctaPath: propCtaPath,
ctaLabel = "Check Availability",
heroTitle = "Internet Service Plans",
heroDescription = "NTT Optical Fiber with full English support",
}: PublicInternetPlansContentProps) {
const { data: catalog, isLoading, error } = useInternetCatalog();
const shopBasePath = useShopBasePath();
const ctaPath = `${shopBasePath}/internet/configure`;
const servicesBasePath = useServicesBasePath();
const defaultCtaPath = `${servicesBasePath}/internet/configure`;
const ctaPath = propCtaPath ?? defaultCtaPath;
const [openFaqIndex, setOpenFaqIndex] = useState<number | null>(null);
const internetFeatures: HighlightFeature[] = [
{
icon: <WifiIcon className="h-6 w-6" />,
title: "NTT Optical Fiber",
description: "Japan's most reliable network with speeds up to 10Gbps",
highlight: "99.9% uptime",
},
{
icon: <BoltIcon className="h-6 w-6" />,
title: "IPv6/IPoE Ready",
description: "Next-gen protocol for congestion-free browsing",
highlight: "No peak-hour slowdowns",
},
{
icon: <ChatBubbleLeftRightIcon className="h-6 w-6" />,
title: "Full English Support",
description: "Native English service for setup, billing & technical help",
highlight: "No language barriers",
},
{
icon: <DocumentTextIcon className="h-6 w-6" />,
title: "One Bill, One Provider",
description: "NTT line + ISP + equipment bundled with simple billing",
highlight: "No hidden fees",
},
{
icon: <WrenchScrewdriverIcon className="h-6 w-6" />,
title: "On-site Support",
description: "Technicians can visit for installation & troubleshooting",
highlight: "Professional setup",
},
{
icon: <GlobeAltIcon className="h-6 w-6" />,
title: "Flexible Options",
description: "Multiple ISP configs available, IPv4/PPPoE if needed",
highlight: "Customizable",
},
];
// Group catalog items by offering type
const groupedOfferings = useMemo(() => {
if (!catalog?.plans) return [];
@ -289,7 +345,7 @@ export function PublicInternetPlansView() {
if (error) {
return (
<div className="space-y-6">
<CatalogBackLink href={shopBasePath} />
<CatalogBackLink href={servicesBasePath} />
<AlertBanner variant="error" title="Unable to load plans">
We couldn&apos;t load internet plans. Please try again later.
</AlertBanner>
@ -300,20 +356,18 @@ export function PublicInternetPlansView() {
return (
<div className="space-y-6">
{/* Back link */}
<CatalogBackLink href={shopBasePath} />
<CatalogBackLink href={servicesBasePath} />
{/* Hero - Clean and impactful */}
<div className="text-center py-4">
<h1 className="text-3xl md:text-4xl font-bold text-foreground tracking-tight">
Internet Service Plans
{heroTitle}
</h1>
<p className="text-base text-muted-foreground mt-2 max-w-lg mx-auto">
NTT Optical Fiber with full English support
</p>
<p className="text-base text-muted-foreground mt-2 max-w-lg mx-auto">{heroDescription}</p>
</div>
{/* Why choose us - 3 pillars */}
<WhyChooseUsPillars />
{/* Service Highlights */}
<ServiceHighlights features={internetFeatures} />
{/* Connection types - no extra header text */}
<section className="space-y-3">
@ -338,6 +392,8 @@ export function PublicInternetPlansView() {
tiers={offering.tiers}
isPremium={offering.isPremium}
ctaPath={ctaPath}
customCtaLabel={ctaLabel}
onCtaClick={onCtaClick}
defaultExpanded={index === 0}
showConnectionInfo={offering.showConnectionInfo}
/>
@ -356,9 +412,20 @@ export function PublicInternetPlansView() {
<p className="text-sm text-muted-foreground mb-5 max-w-sm mx-auto">
Enter your address to see what&apos;s available at your location
</p>
<Button as="a" href={ctaPath} size="lg" rightIcon={<ArrowRightIcon className="h-4 w-4" />}>
Check Availability
</Button>
{onCtaClick ? (
<Button onClick={onCtaClick} size="lg" rightIcon={<ArrowRightIcon className="h-4 w-4" />}>
{ctaLabel}
</Button>
) : (
<Button
as="a"
href={ctaPath}
size="lg"
rightIcon={<ArrowRightIcon className="h-4 w-4" />}
>
{ctaLabel}
</Button>
)}
</section>
{/* FAQ Section */}
@ -380,6 +447,14 @@ export function PublicInternetPlansView() {
);
}
/**
* Public Internet Plans page - Marketing/Conversion focused
* Clean, polished design optimized for conversion
*/
export function PublicInternetPlansView() {
return <PublicInternetPlansContent />;
}
// Helper functions
function getSpeedBadge(offeringType: string): string {
const speeds: Record<string, string> = {

View File

@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation";
import { DevicePhoneMobileIcon, CheckIcon, BoltIcon } from "@heroicons/react/24/outline";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import { useSimPlan } from "@/features/catalog/hooks";
import { InlineAuthSection } from "@/features/auth/components/InlineAuthSection/InlineAuthSection";
import { CardPricing } from "@/features/catalog/components/base/CardPricing";
@ -17,19 +17,19 @@ import { Skeleton } from "@/components/atoms/loading-skeleton";
* Simplified design focused on quick signup-to-order flow.
*/
export function PublicSimConfigureView() {
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const searchParams = useSearchParams();
const planSku = searchParams?.get("planSku");
const { plan, isLoading } = useSimPlan(planSku || undefined);
const redirectTarget = planSku
? `/account/shop/sim/configure?planSku=${encodeURIComponent(planSku)}`
: "/account/shop/sim";
? `/account/services/sim/configure?planSku=${encodeURIComponent(planSku)}`
: "/account/services/sim";
if (isLoading) {
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
<CatalogBackLink href={`${shopBasePath}/sim`} label="Back to SIM plans" />
<CatalogBackLink href={`${servicesBasePath}/sim`} label="Back to SIM plans" />
<div className="mt-8 space-y-6">
<Skeleton className="h-10 w-96 mx-auto" />
<Skeleton className="h-32 w-full" />
@ -41,7 +41,7 @@ export function PublicSimConfigureView() {
if (!plan) {
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
<CatalogBackLink href={`${shopBasePath}/sim`} label="Back to SIM plans" />
<CatalogBackLink href={`${servicesBasePath}/sim`} label="Back to SIM plans" />
<AlertBanner variant="error" title="Plan not found">
The selected plan could not be found. Please go back and select a plan.
</AlertBanner>
@ -51,7 +51,7 @@ export function PublicSimConfigureView() {
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
<CatalogBackLink href={`${shopBasePath}/sim`} label="Back to SIM plans" />
<CatalogBackLink href={`${servicesBasePath}/sim`} label="Back to SIM plans" />
{/* Header */}
<div className="mt-6 mb-8 text-center">
@ -144,15 +144,15 @@ export function PublicSimConfigureView() {
)}
</div>
{/* Quick order info */}
<div className="mb-8 bg-success-soft border border-success/25 rounded-xl p-4">
{/* Order process info */}
<div className="mb-8 bg-info/10 border border-info/25 rounded-xl p-4">
<div className="flex items-start gap-3">
<BoltIcon className="h-5 w-5 text-success mt-0.5 flex-shrink-0" />
<BoltIcon className="h-5 w-5 text-info mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-foreground">Order today, get started fast</p>
<p className="text-sm font-medium text-foreground">How ordering works</p>
<p className="text-sm text-muted-foreground mt-1">
After signup, add a payment method and configure your SIM options. Choose eSIM for
instant activation or physical SIM (ships next business day).
After signup, add a payment method and upload your residence card for verification.
We'll review your application within 1-2 business days and notify you once approved.
</p>
</div>
</div>

View File

@ -7,17 +7,28 @@ import {
PhoneIcon,
GlobeAltIcon,
ArrowLeftIcon,
BoltIcon,
SignalIcon,
SparklesIcon,
ChevronDownIcon,
InformationCircleIcon,
BanknotesIcon,
ExclamationTriangleIcon,
CreditCardIcon,
CalendarDaysIcon,
PhoneArrowDownLeftIcon,
} from "@heroicons/react/24/outline";
import { Skeleton } from "@/components/atoms/loading-skeleton";
import { Button } from "@/components/atoms/button";
import { useSimCatalog } from "@/features/catalog/hooks";
import type { SimCatalogProduct } from "@customer-portal/domain/catalog";
import { SimPlanTypeSection } from "@/features/catalog/components/sim/SimPlanTypeSection";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { CatalogHero } from "@/features/catalog/components/base/CatalogHero";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import { CardPricing } from "@/features/catalog/components/base/CardPricing";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import {
ServiceHighlights,
HighlightFeature,
} from "@/features/catalog/components/base/ServiceHighlights";
interface PlansByType {
DataOnly: SimCatalogProduct[];
@ -25,42 +36,157 @@ interface PlansByType {
VoiceOnly: SimCatalogProduct[];
}
// Collapsible section component
function CollapsibleSection({
title,
icon: Icon,
defaultOpen = false,
children,
}: {
title: string;
icon: React.ElementType;
defaultOpen?: boolean;
children: React.ReactNode;
}) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="border border-border rounded-xl overflow-hidden bg-card">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-3">
<Icon className="w-5 h-5 text-primary" />
<span className="font-medium text-foreground">{title}</span>
</div>
<ChevronDownIcon
className={`w-5 h-5 text-muted-foreground transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
/>
</button>
<div
className={`overflow-hidden transition-all duration-300 ${isOpen ? "max-h-[2000px]" : "max-h-0"}`}
>
<div className="p-4 pt-0 border-t border-border">{children}</div>
</div>
</div>
);
}
// Compact plan card component for a cleaner grid
function SimPlanCardCompact({
plan,
onSelect,
}: {
plan: SimCatalogProduct;
onSelect: (sku: string) => void;
}) {
const displayPrice = plan.monthlyPrice ?? plan.unitPrice ?? plan.oneTimePrice ?? 0;
return (
<div className="group relative bg-card border border-border rounded-2xl p-5 hover:border-primary/50 hover:shadow-[var(--cp-shadow-2)] transition-all duration-200">
{/* Data Size Badge */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
<SignalIcon className="w-5 h-5 text-primary" />
</div>
<span className="text-lg font-bold text-foreground">{plan.simDataSize}</span>
</div>
</div>
{/* Price */}
<div className="mb-4">
<CardPricing monthlyPrice={displayPrice} size="sm" alignment="left" />
</div>
{/* Plan name */}
<p className="text-sm text-muted-foreground mb-5 line-clamp-2">{plan.name}</p>
{/* CTA */}
<Button
className="w-full group-hover:bg-primary group-hover:text-primary-foreground"
variant="outline"
onClick={() => onSelect(plan.sku)}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Select Plan
</Button>
</div>
);
}
/**
* Public SIM Plans View
*
* Displays SIM plans for unauthenticated users.
* Simplified version without active subscription checks.
* Clean, focused design with plan selection.
*/
export function PublicSimPlansView() {
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const { data, isLoading, error } = useSimCatalog();
const simPlans: SimCatalogProduct[] = useMemo(() => data?.plans ?? [], [data?.plans]);
const [activeTab, setActiveTab] = useState<"data-voice" | "data-only" | "voice-only">(
"data-voice"
);
const buildRedirect = (planSku?: string) => {
return planSku ? `/shop/sim/configure?planSku=${encodeURIComponent(planSku)}` : "/shop/sim";
const handleSelectPlan = (planSku: string) => {
window.location.href = `${servicesBasePath}/sim/configure?planSku=${encodeURIComponent(planSku)}`;
};
const simFeatures: HighlightFeature[] = [
{
icon: <SignalIcon className="h-6 w-6" />,
title: "NTT Docomo Network",
description: "Best area coverage among the main three carriers in Japan",
highlight: "Nationwide coverage",
},
{
icon: <BanknotesIcon className="h-6 w-6" />,
title: "First Month Free",
description: "Basic fee waived on signup to get you started risk-free",
highlight: "Great value",
},
{
icon: <CreditCardIcon className="h-6 w-6" />,
title: "Foreign Cards Accepted",
description: "We accept both foreign and Japanese credit cards",
highlight: "No hassle",
},
{
icon: <CalendarDaysIcon className="h-6 w-6" />,
title: "No Binding Contract",
description: "Minimum 4 months service (1st month free + 3 billing months)",
highlight: "Flexible contract",
},
{
icon: <PhoneArrowDownLeftIcon className="h-6 w-6" />,
title: "Number Portability",
description: "Easily switch to us keeping your current Japanese number",
highlight: "Keep your number",
},
{
icon: <DevicePhoneMobileIcon className="h-6 w-6" />,
title: "Free Plan Changes",
description: "Switch data plans anytime for the next billing cycle",
highlight: "Flexibility",
},
];
if (isLoading) {
return (
<div className="max-w-6xl mx-auto px-4">
<CatalogBackLink href={shopBasePath} label="Back to Services" />
<div className="text-center mb-12">
<div className="max-w-6xl mx-auto px-4 pb-16">
<CatalogBackLink href={servicesBasePath} label="Back to Services" />
<div className="text-center mb-12 pt-8">
<Skeleton className="h-10 w-80 mx-auto mb-4" />
<Skeleton className="h-6 w-[36rem] max-w-full mx-auto" />
<Skeleton className="h-6 w-96 max-w-full mx-auto" />
</div>
<div className="mb-8 flex justify-center">
<Skeleton className="h-10 w-[32rem] max-w-full" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="bg-card rounded-xl border border-border p-6 space-y-3">
<Skeleton className="h-6 w-1/2" />
<Skeleton className="h-4 w-2/3" />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="bg-card rounded-2xl border border-border p-5 space-y-4">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-6 w-20" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-10 w-full" />
</div>
))}
@ -73,15 +199,10 @@ export function PublicSimPlansView() {
const errorMessage = error instanceof Error ? error.message : "An unexpected error occurred";
return (
<div className="max-w-6xl mx-auto px-4">
<div className="rounded-lg bg-destructive/10 border border-destructive/20 p-6">
<div className="text-destructive font-medium">Failed to load SIM plans</div>
<div className="text-destructive/80 text-sm mt-1">{errorMessage}</div>
<Button
as="a"
href={shopBasePath}
className="mt-4"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
<div className="rounded-xl bg-destructive/10 border border-destructive/20 p-8 text-center">
<div className="text-destructive font-medium text-lg mb-2">Failed to load SIM plans</div>
<div className="text-destructive/80 text-sm mb-6">{errorMessage}</div>
<Button as="a" href={servicesBasePath} leftIcon={<ArrowLeftIcon className="w-4 h-4" />}>
Back to Services
</Button>
</div>
@ -100,228 +221,331 @@ export function PublicSimPlansView() {
{ DataOnly: [], DataSmsVoice: [], VoiceOnly: [] }
);
const currentPlans =
activeTab === "data-voice"
? plansByType.DataSmsVoice
: activeTab === "data-only"
? plansByType.DataOnly
: plansByType.VoiceOnly;
return (
<div className="max-w-6xl mx-auto px-4 pb-16">
<CatalogBackLink href={shopBasePath} label="Back to Services" />
<CatalogBackLink href={servicesBasePath} label="Back to Services" />
<CatalogHero
title="SIM & eSIM Plans"
description="Data, voice, and SMS plans with 5G network coverage."
>
{/* Order info banner */}
<div className="bg-success-soft border border-success/25 rounded-xl px-4 py-3 max-w-xl mt-4">
<div className="flex items-center gap-2 justify-center">
<BoltIcon className="h-4 w-4 text-success flex-shrink-0" />
<p className="text-sm text-foreground">
<span className="font-medium">Order today</span>
<span className="text-muted-foreground">
{" "}
eSIM activates instantly, physical SIM ships next business day.
</span>
</p>
</div>
{/* Hero Section - Clean & Minimal */}
<div className="text-center pt-8 pb-6">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 mb-6">
<SparklesIcon className="w-4 h-4 text-primary" />
<span className="text-sm font-medium text-primary">Powered by NTT DOCOMO</span>
</div>
</CatalogHero>
<h1 className="text-3xl md:text-4xl font-bold text-foreground mb-4">Mobile SIM Plans</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Get connected with Japan's best network coverage. Choose eSIM for quick digital delivery
or physical SIM shipped to your door.
</p>
</div>
<div className="mb-8 flex justify-center">
<div className="border-b border-border">
<nav className="-mb-px flex space-x-6" aria-label="Tabs">
<button
onClick={() => setActiveTab("data-voice")}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors duration-200 ${
activeTab === "data-voice"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
}`}
>
<PhoneIcon className="h-5 w-5" />
Data + SMS + Voice
{plansByType.DataSmsVoice.length > 0 && (
<span
className={`text-xs font-semibold px-2 py-0.5 rounded-full border ${
activeTab === "data-voice"
? "border-primary/20 bg-primary/10 text-primary"
: "border-border text-muted-foreground"
}`}
>
{plansByType.DataSmsVoice.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab("data-only")}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors duration-200 ${
activeTab === "data-only"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
}`}
>
<GlobeAltIcon className="h-5 w-5" />
Data Only
{plansByType.DataOnly.length > 0 && (
<span
className={`text-xs font-semibold px-2 py-0.5 rounded-full border ${
activeTab === "data-only"
? "border-primary/20 bg-primary/10 text-primary"
: "border-border text-muted-foreground"
}`}
>
{plansByType.DataOnly.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab("voice-only")}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors duration-200 ${
activeTab === "voice-only"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
}`}
>
<CheckIcon className="h-5 w-5" />
Voice Only
{plansByType.VoiceOnly.length > 0 && (
<span
className={`text-xs font-semibold px-2 py-0.5 rounded-full border ${
activeTab === "voice-only"
? "border-primary/20 bg-primary/10 text-primary"
: "border-border text-muted-foreground"
}`}
>
{plansByType.VoiceOnly.length}
</span>
)}
</button>
</nav>
{/* Service Highlights */}
<ServiceHighlights features={simFeatures} className="mb-12" />
{/* Plan Type Tabs */}
<div className="flex justify-center mb-8 mt-6">
<div className="inline-flex rounded-xl bg-muted p-1 border border-border">
<button
onClick={() => setActiveTab("data-voice")}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
activeTab === "data-voice"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
<PhoneIcon className="h-4 w-4" />
<span className="hidden sm:inline">Data + Voice</span>
<span className="sm:hidden">All-in</span>
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
{plansByType.DataSmsVoice.length}
</span>
</button>
<button
onClick={() => setActiveTab("data-only")}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
activeTab === "data-only"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
<GlobeAltIcon className="h-4 w-4" />
<span className="hidden sm:inline">Data Only</span>
<span className="sm:hidden">Data</span>
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
{plansByType.DataOnly.length}
</span>
</button>
<button
onClick={() => setActiveTab("voice-only")}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
activeTab === "voice-only"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
<CheckIcon className="h-4 w-4" />
<span className="hidden sm:inline">Voice Only</span>
<span className="sm:hidden">Voice</span>
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
{plansByType.VoiceOnly.length}
</span>
</button>
</div>
</div>
<div id="plans" className="min-h-[500px] relative">
{activeTab === "data-voice" && (
<div className="animate-in fade-in duration-300">
<SimPlanTypeSection
title="Data + SMS + Voice Plans"
description="Comprehensive plans with high-speed data, messaging, and calling"
icon={<DevicePhoneMobileIcon className="h-6 w-6 text-primary" />}
plans={plansByType.DataSmsVoice}
showFamilyDiscount={false}
cardAction={plan => ({ label: "Continue", href: buildRedirect(plan.sku) })}
/>
{/* Plan Cards Grid */}
<div id="plans" className="min-h-[300px]">
{currentPlans.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 animate-in fade-in duration-300">
{currentPlans.map(plan => (
<SimPlanCardCompact key={plan.id} plan={plan} onSelect={handleSelectPlan} />
))}
</div>
)}
{activeTab === "data-only" && (
<div className="animate-in fade-in duration-300">
<SimPlanTypeSection
title="Data Only Plans"
description="Flexible data-only plans for internet usage"
icon={<GlobeAltIcon className="h-6 w-6 text-primary" />}
plans={plansByType.DataOnly}
showFamilyDiscount={false}
cardAction={plan => ({ label: "Continue", href: buildRedirect(plan.sku) })}
/>
</div>
)}
{activeTab === "voice-only" && (
<div className="animate-in fade-in duration-300">
<SimPlanTypeSection
title="Voice + SMS Only Plans"
description="Plans focused on voice calling and messaging without data bundles"
icon={<PhoneIcon className="h-6 w-6 text-primary" />}
plans={plansByType.VoiceOnly}
showFamilyDiscount={false}
cardAction={plan => ({ label: "Continue", href: buildRedirect(plan.sku) })}
/>
) : (
<div className="text-center py-16 text-muted-foreground">
No plans available in this category.
</div>
)}
</div>
<div className="mt-8 bg-muted/50 rounded-2xl p-8 max-w-4xl mx-auto">
<h3 className="font-bold text-foreground text-xl mb-6 text-center">
Plan Features & Terms
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 text-sm">
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-success mt-0.5 flex-shrink-0" />
{/* Collapsible Information Sections */}
<div className="mt-12 space-y-4">
{/* Calling & SMS Rates */}
<CollapsibleSection title="Calling & SMS Rates" icon={PhoneIcon}>
<div className="space-y-6 pt-4">
{/* Domestic Rates */}
<div>
<div className="font-medium text-foreground">3-Month Contract</div>
<div className="text-muted-foreground">Minimum 3 billing months</div>
<h4 className="text-sm font-medium text-foreground mb-3 flex items-center gap-2">
<span className="w-5 h-3 rounded-sm bg-[#BC002D] relative overflow-hidden flex items-center justify-center">
<span className="w-2 h-2 rounded-full bg-white" />
</span>
Domestic (Japan)
</h4>
<div className="grid grid-cols-2 gap-4">
<div className="bg-muted/50 rounded-lg p-4 border border-border">
<div className="text-sm text-muted-foreground mb-1">Voice Calls</div>
<div className="text-xl font-bold text-foreground">
¥10<span className="text-sm font-normal text-muted-foreground">/30 sec</span>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4 border border-border">
<div className="text-sm text-muted-foreground mb-1">SMS</div>
<div className="text-xl font-bold text-foreground">
¥3<span className="text-sm font-normal text-muted-foreground">/message</span>
</div>
</div>
</div>
<p className="text-xs text-muted-foreground mt-2">
Incoming calls and SMS are free. Pay-per-use charges billed 5-6 weeks after usage.
</p>
</div>
{/* Unlimited Option */}
<div className="p-4 bg-success/5 border border-success/20 rounded-lg">
<div className="flex items-start gap-3">
<PhoneIcon className="w-5 h-5 text-success mt-0.5" />
<div>
<h4 className="font-medium text-foreground">Unlimited Domestic Calling</h4>
<p className="text-sm text-muted-foreground">
Add unlimited domestic calls for{" "}
<span className="font-semibold text-success">¥3,000/month</span> (available at
checkout)
</p>
</div>
</div>
</div>
{/* International Note */}
<div className="text-sm text-muted-foreground">
<p>
International calling rates vary by country (¥31-148/30 sec). See{" "}
<a
href="https://www.docomo.ne.jp/service/world/worldcall/call/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
NTT Docomo's website
</a>{" "}
for full details.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-success mt-0.5 flex-shrink-0" />
</CollapsibleSection>
{/* Fees & Discounts */}
<CollapsibleSection title="Fees & Discounts" icon={BanknotesIcon}>
<div className="space-y-6 pt-4">
{/* Fees */}
<div>
<div className="font-medium text-foreground">First Month Free</div>
<div className="text-muted-foreground">Basic fee waived initially</div>
<h4 className="text-sm font-medium text-foreground mb-3">One-time Fees</h4>
<div className="space-y-3">
<div className="flex items-center justify-between py-2 border-b border-border">
<span className="text-muted-foreground">Activation Fee</span>
<span className="font-medium text-foreground">¥1,500</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-border">
<span className="text-muted-foreground">SIM Replacement (lost/damaged)</span>
<span className="font-medium text-foreground">¥1,500</span>
</div>
<div className="flex items-center justify-between py-2">
<span className="text-muted-foreground">eSIM Re-download</span>
<span className="font-medium text-foreground">¥1,500</span>
</div>
</div>
</div>
{/* Discounts */}
<div className="p-4 bg-success/5 border border-success/20 rounded-lg">
<h4 className="font-medium text-foreground mb-2">Family Discount</h4>
<p className="text-sm text-muted-foreground">
<span className="font-semibold text-success">¥300/month off</span> per additional
Voice SIM on your account
</p>
</div>
<p className="text-xs text-muted-foreground">All prices exclude 10% consumption tax.</p>
</div>
</CollapsibleSection>
{/* Important Information & Terms */}
<CollapsibleSection title="Important Information & Terms" icon={InformationCircleIcon}>
<div className="space-y-6 pt-4 text-sm">
{/* Key Notices */}
<div>
<h4 className="font-medium text-foreground mb-3 flex items-center gap-2">
<ExclamationTriangleIcon className="w-4 h-4 text-warning" />
Important Notices
</h4>
<ul className="space-y-2 text-muted-foreground">
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
ID verification with official documents (name, date of birth, address, photo) is
required during checkout.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
A compatible unlocked device is required. Check compatibility on our website.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
Service may not be available in areas with weak signal. See{" "}
<a
href="https://www.nttdocomo.co.jp/English/support/area/index.html"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
NTT Docomo coverage map
</a>
.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
SIM is activated as 4G by default. 5G can be requested via your account portal.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
International data roaming is not available. Voice/SMS roaming can be enabled
upon request (¥50,000/month limit).
</span>
</li>
</ul>
</div>
{/* Contract Terms */}
<div>
<h4 className="font-medium text-foreground mb-3">Contract Terms</h4>
<ul className="space-y-2 text-muted-foreground">
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
<strong className="text-foreground">Minimum contract:</strong> 3 full billing
months. First month (sign-up to end of month) is free and doesn't count.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
<strong className="text-foreground">Billing cycle:</strong> 1st to end of month.
Regular billing starts the 1st of the following month after sign-up.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
<strong className="text-foreground">Cancellation:</strong> Can be requested
after 3rd month via cancellation form. Monthly fee is incurred in full for
cancellation month.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
<strong className="text-foreground">SIM return:</strong> SIM card must be
returned after service termination.
</span>
</li>
</ul>
</div>
{/* Additional Options */}
<div>
<h4 className="font-medium text-foreground mb-3">Additional Options</h4>
<ul className="space-y-2 text-muted-foreground">
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>Call waiting and voice mail available as separate paid options.</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>Data plan changes are free and take effect next billing month.</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
Voice plan changes require new SIM issuance and standard policies apply.
</span>
</li>
</ul>
</div>
{/* Disclaimer */}
<div className="p-4 bg-muted/50 rounded-lg">
<p className="text-xs text-muted-foreground">
Payment is by credit card only. Data service is not suitable for activities
requiring continuous large data transfers. See full Terms of Service for complete
details.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-success mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-foreground">5G Network</div>
<div className="text-muted-foreground">High-speed coverage</div>
</div>
</div>
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-success mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-foreground">eSIM Support</div>
<div className="text-muted-foreground">Digital activation</div>
</div>
</div>
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-success mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-foreground">Family Discounts</div>
<div className="text-muted-foreground">Multi-line savings (after sign-in)</div>
</div>
</div>
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-success mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-foreground">Plan Switching</div>
<div className="text-muted-foreground">Free data plan changes</div>
</div>
</div>
</div>
</CollapsibleSection>
</div>
<AlertBanner
variant="info"
title="Important Terms & Conditions"
className="mt-8 max-w-4xl mx-auto"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div className="space-y-3">
<div>
<div className="font-medium">Contract Period</div>
<p>
Minimum 3 full billing months required. First month (sign-up to end of month) is
free and doesn&apos;t count toward contract.
</p>
</div>
<div>
<div className="font-medium">Billing Cycle</div>
<p>
Monthly billing from 1st to end of month. Regular billing starts on 1st of following
month after sign-up.
</p>
</div>
</div>
<div className="space-y-3">
<div>
<div className="font-medium">Plan Changes</div>
<p>
Data plan switching is free and takes effect next month. Voice plan changes require
new SIM.
</p>
</div>
<div>
<div className="font-medium">SIM Replacement</div>
<p>Reissue fee of 1,500 JPY applies for damaged, lost, or replacement SIM cards.</p>
</div>
</div>
</div>
</AlertBanner>
{/* Terms Footer */}
<div className="mt-8 text-center text-sm text-muted-foreground">
<p>
All prices exclude 10% consumption tax.{" "}
<a href="#" className="text-primary hover:underline">
View full Terms of Service
</a>
</p>
</div>
</div>
);
}

View File

@ -8,7 +8,7 @@ import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { VpnPlanCard } from "@/features/catalog/components/vpn/VpnPlanCard";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { CatalogHero } from "@/features/catalog/components/base/CatalogHero";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
/**
* Public VPN Plans View
@ -16,7 +16,7 @@ import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
* Displays VPN plans for unauthenticated users.
*/
export function PublicVpnPlansView() {
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const { data, isLoading, error } = useVpnCatalog();
const vpnPlans = data?.plans || [];
const activationFees = data?.activationFees || [];
@ -24,7 +24,7 @@ export function PublicVpnPlansView() {
if (isLoading || error) {
return (
<div className="max-w-6xl mx-auto px-4">
<CatalogBackLink href={shopBasePath} label="Back to Services" />
<CatalogBackLink href={servicesBasePath} label="Back to Services" />
<AsyncBlock
isLoading={isLoading}
@ -44,7 +44,7 @@ export function PublicVpnPlansView() {
return (
<div className="max-w-6xl mx-auto px-4 pb-16">
<CatalogBackLink href={shopBasePath} label="Back to Services" />
<CatalogBackLink href={servicesBasePath} label="Back to Services" />
<CatalogHero
title="VPN Router Service"
@ -92,7 +92,7 @@ export function PublicVpnPlansView() {
We couldn&apos;t find any VPN plans available at this time.
</p>
<CatalogBackLink
href={shopBasePath}
href={servicesBasePath}
label="Back to Services"
align="center"
className="mt-4 mb-0"

View File

@ -8,21 +8,31 @@ import {
PhoneIcon,
GlobeAltIcon,
ArrowLeftIcon,
SignalIcon,
SparklesIcon,
CreditCardIcon,
ChevronDownIcon,
InformationCircleIcon,
BanknotesIcon,
ExclamationTriangleIcon,
CalendarDaysIcon,
PhoneArrowDownLeftIcon,
} from "@heroicons/react/24/outline";
import { Skeleton } from "@/components/atoms/loading-skeleton";
import { Button } from "@/components/atoms/button";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { useSimCatalog } from "@/features/catalog/hooks";
import type { SimCatalogProduct } from "@customer-portal/domain/catalog";
import { SimPlanTypeSection } from "@/features/catalog/components/sim/SimPlanTypeSection";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { CatalogHero } from "@/features/catalog/components/base/CatalogHero";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
import { usePaymentMethods } from "@/features/billing/hooks/useBilling";
import { CardPricing } from "@/features/catalog/components/base/CardPricing";
import { ArrowRightIcon, UsersIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/navigation";
import {
useResidenceCardVerification,
useSubmitResidenceCard,
} from "@/features/verification/hooks/useResidenceCardVerification";
ServiceHighlights,
HighlightFeature,
} from "@/features/catalog/components/base/ServiceHighlights";
interface PlansByType {
DataOnly: SimCatalogProduct[];
@ -30,8 +40,110 @@ interface PlansByType {
VoiceOnly: SimCatalogProduct[];
}
// Collapsible section component
function CollapsibleSection({
title,
icon: Icon,
defaultOpen = false,
children,
}: {
title: string;
icon: React.ElementType;
defaultOpen?: boolean;
children: React.ReactNode;
}) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="border border-border rounded-xl overflow-hidden bg-card">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-3">
<Icon className="w-5 h-5 text-primary" />
<span className="font-medium text-foreground">{title}</span>
</div>
<ChevronDownIcon
className={`w-5 h-5 text-muted-foreground transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
/>
</button>
<div
className={`overflow-hidden transition-all duration-300 ${isOpen ? "max-h-[2000px]" : "max-h-0"}`}
>
<div className="p-4 pt-0 border-t border-border">{children}</div>
</div>
</div>
);
}
// Compact plan card component
function SimPlanCardCompact({
plan,
isFamily,
onSelect,
}: {
plan: SimCatalogProduct;
isFamily?: boolean;
onSelect: (sku: string) => void;
}) {
const displayPrice = plan.monthlyPrice ?? plan.unitPrice ?? plan.oneTimePrice ?? 0;
return (
<div
className={`group relative bg-card rounded-2xl p-5 transition-all duration-200 ${
isFamily
? "border-2 border-success/50 hover:border-success hover:shadow-[var(--cp-shadow-2)]"
: "border border-border hover:border-primary/50 hover:shadow-[var(--cp-shadow-2)]"
}`}
>
{/* Family Discount Badge */}
{isFamily && (
<div className="absolute -top-3 left-4 flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-success text-success-foreground text-xs font-medium">
<UsersIcon className="w-3.5 h-3.5" />
Family Discount
</div>
)}
{/* Data Size Badge */}
<div className="flex items-center justify-between mb-4 mt-1">
<div className="flex items-center gap-2">
<div
className={`w-10 h-10 rounded-xl flex items-center justify-center ${isFamily ? "bg-success/10" : "bg-primary/10"}`}
>
<SignalIcon className={`w-5 h-5 ${isFamily ? "text-success" : "text-primary"}`} />
</div>
<span className="text-lg font-bold text-foreground">{plan.simDataSize}</span>
</div>
</div>
{/* Price */}
<div className="mb-4">
<CardPricing monthlyPrice={displayPrice} size="sm" alignment="left" />
{isFamily && (
<div className="text-xs text-success font-medium mt-1">Discounted price applied</div>
)}
</div>
{/* Plan name */}
<p className="text-sm text-muted-foreground mb-5 line-clamp-2">{plan.name}</p>
{/* CTA */}
<Button
className={`w-full ${isFamily ? "group-hover:bg-success group-hover:text-success-foreground" : "group-hover:bg-primary group-hover:text-primary-foreground"}`}
variant="outline"
onClick={() => onSelect(plan.sku)}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Select Plan
</Button>
</div>
);
}
export function SimPlansContainer() {
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const router = useRouter();
const { data, isLoading, error } = useSimCatalog();
const simPlans: SimCatalogProduct[] = useMemo(() => data?.plans ?? [], [data?.plans]);
const [hasExistingSim, setHasExistingSim] = useState(false);
@ -39,72 +151,79 @@ export function SimPlansContainer() {
"data-voice"
);
const { data: paymentMethods, isLoading: paymentMethodsLoading } = usePaymentMethods();
const { data: residenceCard, isLoading: residenceCardLoading } = useResidenceCardVerification();
const submitResidenceCard = useSubmitResidenceCard();
const [residenceCardFile, setResidenceCardFile] = useState<File | null>(null);
useEffect(() => {
setHasExistingSim(simPlans.some(p => p.simHasFamilyDiscount));
}, [simPlans]);
const handleSelectPlan = (planSku: string) => {
router.push(`${servicesBasePath}/sim/configure?planSku=${encodeURIComponent(planSku)}`);
};
const simFeatures: HighlightFeature[] = [
{
icon: <SignalIcon className="h-6 w-6" />,
title: "NTT Docomo Network",
description: "Best area coverage among the main three carriers in Japan",
highlight: "Nationwide coverage",
},
{
icon: <BanknotesIcon className="h-6 w-6" />,
title: "First Month Free",
description: "Basic fee waived on signup to get you started risk-free",
highlight: "Great value",
},
{
icon: <CreditCardIcon className="h-6 w-6" />,
title: "Foreign Cards Accepted",
description: "We accept both foreign and Japanese credit cards",
highlight: "No hassle",
},
{
icon: <CalendarDaysIcon className="h-6 w-6" />,
title: "No Binding Contract",
description: "Minimum 4 months service (1st month free + 3 billing months)",
highlight: "Flexible contract",
},
{
icon: <PhoneArrowDownLeftIcon className="h-6 w-6" />,
title: "Number Portability",
description: "Easily switch to us keeping your current Japanese number",
highlight: "Keep your number",
},
{
icon: <DevicePhoneMobileIcon className="h-6 w-6" />,
title: "Free Plan Changes",
description: "Switch data plans anytime for the next billing cycle",
highlight: "Flexibility",
},
];
if (isLoading) {
return (
<div className="min-h-screen bg-slate-50">
<PageLayout
title="SIM Plans"
description="Loading plans..."
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto px-4">
<CatalogBackLink href={shopBasePath} label="Back to Services" />
{/* Title block */}
<div className="text-center mb-12">
<div className="h-10 w-80 bg-gray-200 rounded mx-auto mb-4" />
<div className="h-6 w-[36rem] max-w-full bg-gray-200 rounded mx-auto" />
</div>
{/* Family discount banner slot */}
<div className="mb-8">
<div className="h-20 w-full bg-green-50 border border-green-200 rounded-xl" />
</div>
{/* Tabs */}
<div className="mb-8 flex justify-center">
<div className="h-10 w-[32rem] max-w-full bg-gray-200 rounded" />
</div>
{/* Plans grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="bg-white rounded-xl border border-gray-200 p-6 space-y-3">
<Skeleton className="h-6 w-1/2" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-10 w-full" />
</div>
))}
</div>
{/* Terms section */}
<div className="mt-8 bg-gray-50 rounded-2xl p-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="flex items-start gap-3">
<div className="h-5 w-5 bg-gray-200 rounded" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-3 w-56" />
</div>
</div>
))}
</div>
</div>
{/* Important terms banner */}
<div className="mt-8 h-28 bg-yellow-50 border border-yellow-200 rounded-xl" />
<PageLayout
title="SIM Plans"
description="Loading plans..."
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto px-4 pb-16">
<CatalogBackLink href={servicesBasePath} label="Back to Services" />
<div className="text-center mb-12 pt-8">
<Skeleton className="h-10 w-80 mx-auto mb-4" />
<Skeleton className="h-6 w-96 max-w-full mx-auto" />
</div>
</PageLayout>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="bg-card rounded-2xl border border-border p-5 space-y-4">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-6 w-20" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-10 w-full" />
</div>
))}
</div>
</div>
</PageLayout>
);
}
@ -116,15 +235,10 @@ export function SimPlansContainer() {
description="Error loading plans"
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="rounded-lg bg-red-50 border border-red-200 p-6">
<div className="text-red-800 font-medium">Failed to load SIM plans</div>
<div className="text-red-600 text-sm mt-1">{errorMessage}</div>
<Button
as="a"
href={shopBasePath}
className="mt-4"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
<div className="rounded-xl bg-destructive/10 border border-destructive/20 p-8 text-center">
<div className="text-destructive font-medium text-lg mb-2">Failed to load SIM plans</div>
<div className="text-destructive/80 text-sm mb-6">{errorMessage}</div>
<Button as="a" href={servicesBasePath} leftIcon={<ArrowLeftIcon className="w-4 h-4" />}>
Back to Services
</Button>
</div>
@ -143,35 +257,56 @@ export function SimPlansContainer() {
{ DataOnly: [], DataSmsVoice: [], VoiceOnly: [] }
);
const getCurrentPlans = () => {
const plans =
activeTab === "data-voice"
? plansByType.DataSmsVoice
: activeTab === "data-only"
? plansByType.DataOnly
: plansByType.VoiceOnly;
const regularPlans = plans.filter(p => !p.simHasFamilyDiscount);
const familyPlans = plans.filter(p => p.simHasFamilyDiscount);
return { regularPlans, familyPlans };
};
const { regularPlans, familyPlans } = getCurrentPlans();
return (
<div className="min-h-screen bg-slate-50">
<PageLayout
title="SIM Plans"
description="Choose your mobile plan with flexible options"
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto px-4 pb-16">
<CatalogBackLink href={shopBasePath} label="Back to Services" />
<PageLayout
title="SIM Plans"
description="Choose your mobile plan with flexible options"
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto px-4 pb-16">
<CatalogBackLink href={servicesBasePath} label="Back to Services" />
<CatalogHero
title="Choose Your SIM Plan"
description="Flexible mobile plans with physical SIM and eSIM options for any device."
/>
{/* Hero Section */}
<div className="text-center pt-8 pb-8">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 mb-6">
<SparklesIcon className="w-4 h-4 text-primary" />
<span className="text-sm font-medium text-primary">Powered by NTT DOCOMO</span>
</div>
<h1 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
Choose Your SIM Plan
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Get connected with Japan's best network coverage. Choose eSIM for quick digital delivery
or physical SIM shipped to your door.
</p>
</div>
{paymentMethodsLoading || residenceCardLoading ? (
<AlertBanner variant="info" title="Checking requirements…" className="mb-8">
<p className="text-sm text-foreground/80">
Loading your payment method and residence card verification status.
</p>
{/* Requirements Banners */}
<div className="space-y-4 mb-8">
{paymentMethodsLoading ? (
<AlertBanner variant="info" title="Checking requirements…">
<p className="text-sm text-foreground/80">Loading your payment method status.</p>
</AlertBanner>
) : (
<>
{paymentMethods && paymentMethods.totalCount === 0 && (
<AlertBanner
variant="warning"
title="Add a payment method to order SIM"
className="mb-6"
>
<AlertBanner variant="warning" title="Add a payment method to order SIM">
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<p className="text-sm text-foreground/80">
SIM orders require a saved payment method on your account.
@ -187,312 +322,305 @@ export function SimPlansContainer() {
</div>
</AlertBanner>
)}
{residenceCard?.status === "pending" && (
<AlertBanner variant="info" title="Residence card under review" className="mb-6">
<p className="text-sm text-foreground/80">
Were verifying your residence card. Well update your account once review is
complete.
</p>
</AlertBanner>
)}
{(residenceCard?.status === "not_submitted" ||
residenceCard?.status === "rejected") && (
<AlertBanner
variant={residenceCard?.status === "rejected" ? "warning" : "info"}
title={
residenceCard?.status === "rejected"
? "Residence card needs resubmission"
: "Submit your residence card for verification"
}
className="mb-8"
>
<div className="space-y-3">
<p className="text-sm text-foreground/80">
To order SIM service, please upload your residence card for identity
verification.
</p>
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<input
type="file"
accept="image/*,application/pdf"
onChange={e => setResidenceCardFile(e.target.files?.[0] ?? null)}
className="block w-full sm:max-w-md text-sm text-foreground file:mr-4 file:py-2 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-muted file:text-foreground hover:file:bg-muted/80"
/>
<Button
type="button"
size="sm"
disabled={!residenceCardFile || submitResidenceCard.isPending}
isLoading={submitResidenceCard.isPending}
loadingText="Uploading…"
onClick={() => {
if (!residenceCardFile) return;
submitResidenceCard.mutate(residenceCardFile, {
onSuccess: () => setResidenceCardFile(null),
});
}}
className="sm:ml-auto whitespace-nowrap"
>
Submit for review
</Button>
</div>
{submitResidenceCard.isError && (
<p className="text-sm text-danger">
{submitResidenceCard.error instanceof Error
? submitResidenceCard.error.message
: "Failed to submit residence card."}
</p>
)}
</div>
</AlertBanner>
)}
</>
)}
{hasExistingSim && (
<AlertBanner variant="success" title="Family Discount Applied" className="mb-8">
<div className="space-y-2">
<p>
You already have a SIM subscription with us. Family discount pricing is
automatically applied to eligible additional lines below.
</p>
<ul className="list-disc list-inside">
<li>Reduced monthly pricing automatically reflected</li>
<li>Same great features</li>
<li>Easy to manage multiple lines</li>
</ul>
</div>
<AlertBanner variant="success" title="Family Discount Available">
<p className="text-sm">
You already have a SIM subscription. Discounted pricing is automatically shown for
additional lines.
</p>
</AlertBanner>
)}
<div className="mb-8 flex justify-center">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-6" aria-label="Tabs">
<button
onClick={() => setActiveTab("data-voice")}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors duration-200 ${
activeTab === "data-voice"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<PhoneIcon className="h-5 w-5" />
Data + SMS + Voice
{plansByType.DataSmsVoice.length > 0 && (
<span
className={`text-xs font-semibold px-2 py-0.5 rounded-full border ${
activeTab === "data-voice"
? "border-blue-100 bg-blue-50 text-blue-600"
: "border-gray-200 text-gray-500"
}`}
>
{plansByType.DataSmsVoice.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab("data-only")}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors duration-200 ${
activeTab === "data-only"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<GlobeAltIcon className="h-5 w-5" />
Data Only
{plansByType.DataOnly.length > 0 && (
<span
className={`text-xs font-semibold px-2 py-0.5 rounded-full border ${
activeTab === "data-only"
? "border-blue-100 bg-blue-50 text-blue-600"
: "border-gray-200 text-gray-500"
}`}
>
{plansByType.DataOnly.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab("voice-only")}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors duration-200 ${
activeTab === "voice-only"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<CheckIcon className="h-5 w-5" />
Voice Only
{plansByType.VoiceOnly.length > 0 && (
<span
className={`text-xs font-semibold px-2 py-0.5 rounded-full border ${
activeTab === "voice-only"
? "border-blue-100 bg-blue-50 text-blue-600"
: "border-gray-200 text-gray-500"
}`}
>
{plansByType.VoiceOnly.length}
</span>
)}
</button>
</nav>
</div>
</div>
<div className="min-h-[500px] relative">
{activeTab === "data-voice" && (
<div className="animate-in fade-in duration-300">
<SimPlanTypeSection
title="Data + SMS + Voice Plans"
description={
hasExistingSim
? "Family discount shown where eligible"
: "Comprehensive plans with high-speed data, messaging, and calling"
}
icon={<DevicePhoneMobileIcon className="h-6 w-6 text-blue-600" />}
plans={plansByType.DataSmsVoice}
showFamilyDiscount={hasExistingSim}
/>
</div>
)}
{activeTab === "data-only" && (
<div className="animate-in fade-in duration-300">
<SimPlanTypeSection
title="Data Only Plans"
description={
hasExistingSim
? "Family discount shown where eligible"
: "Flexible data-only plans for internet usage"
}
icon={<GlobeAltIcon className="h-6 w-6 text-purple-600" />}
plans={plansByType.DataOnly}
showFamilyDiscount={hasExistingSim}
/>
</div>
)}
{activeTab === "voice-only" && (
<div className="animate-in fade-in duration-300">
<SimPlanTypeSection
title="Voice + SMS Only Plans"
description={
hasExistingSim
? "Family discount shown where eligible"
: "Plans focused on voice calling and messaging without data bundles"
}
icon={<PhoneIcon className="h-6 w-6 text-orange-600" />}
plans={plansByType.VoiceOnly}
showFamilyDiscount={hasExistingSim}
/>
</div>
)}
</div>
<div className="mt-8 bg-gray-50 rounded-2xl p-8 max-w-4xl mx-auto">
<h3 className="font-bold text-gray-900 text-xl mb-6 text-center">
Plan Features & Terms
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 text-sm">
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-gray-900">3-Month Contract</div>
<div className="text-gray-600">Minimum 3 billing months</div>
</div>
</div>
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-gray-900">First Month Free</div>
<div className="text-gray-600">Basic fee waived initially</div>
</div>
</div>
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-gray-900">5G Network</div>
<div className="text-gray-600">High-speed coverage</div>
</div>
</div>
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-gray-900">eSIM Support</div>
<div className="text-gray-600">Digital activation</div>
</div>
</div>
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-gray-900">Family Discounts</div>
<div className="text-gray-600">Multi-line savings</div>
</div>
</div>
<div className="flex items-start gap-3">
<CheckIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-gray-900">Plan Switching</div>
<div className="text-gray-600">Free data plan changes</div>
</div>
</div>
</div>
</div>
<AlertBanner
variant="info"
title="Important Terms & Conditions"
className="mt-8 max-w-4xl mx-auto"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div className="space-y-3">
<div>
<div className="font-medium">Contract Period</div>
<p>
Minimum 3 full billing months required. First month (sign-up to end of month) is
free and doesn&apos;t count toward contract.
</p>
</div>
<div>
<div className="font-medium">Billing Cycle</div>
<p>
Monthly billing from 1st to end of month. Regular billing starts on 1st of
following month after sign-up.
</p>
</div>
<div>
<div className="font-medium">Cancellation</div>
<p>
Can be requested online after 3rd month. Service terminates at end of billing
cycle.
</p>
</div>
</div>
<div className="space-y-3">
<div>
<div className="font-medium">Plan Changes</div>
<p>
Data plan switching is free and takes effect next month. Voice plan changes
require new SIM and cancellation policies apply.
</p>
</div>
<div>
<div className="font-medium">Calling/SMS Charges</div>
<p>
Pay-per-use charges apply separately. Billed 5-6 weeks after usage within
billing cycle.
</p>
</div>
<div>
<div className="font-medium">SIM Replacement</div>
<p>
Reissue fee of 1,500 JPY applies for damaged, lost, or replacement SIM cards.
</p>
</div>
</div>
</div>
</AlertBanner>
</div>
</PageLayout>
</div>
{/* Service Highlights (Shared with Public View) */}
<ServiceHighlights features={simFeatures} className="mb-12" />
{/* Plan Type Tabs */}
<div className="flex justify-center mb-8">
<div className="inline-flex rounded-xl bg-muted p-1 border border-border">
<button
onClick={() => setActiveTab("data-voice")}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
activeTab === "data-voice"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
<PhoneIcon className="h-4 w-4" />
<span className="hidden sm:inline">Data + Voice</span>
<span className="sm:hidden">All-in</span>
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
{plansByType.DataSmsVoice.length}
</span>
</button>
<button
onClick={() => setActiveTab("data-only")}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
activeTab === "data-only"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
<GlobeAltIcon className="h-4 w-4" />
<span className="hidden sm:inline">Data Only</span>
<span className="sm:hidden">Data</span>
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
{plansByType.DataOnly.length}
</span>
</button>
<button
onClick={() => setActiveTab("voice-only")}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
activeTab === "voice-only"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
<CheckIcon className="h-4 w-4" />
<span className="hidden sm:inline">Voice Only</span>
<span className="sm:hidden">Voice</span>
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
{plansByType.VoiceOnly.length}
</span>
</button>
</div>
</div>
{/* Plan Cards Grid */}
<div id="plans" className="min-h-[300px]">
{regularPlans.length > 0 || familyPlans.length > 0 ? (
<div className="space-y-8 animate-in fade-in duration-300">
{/* Regular Plans */}
{regularPlans.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
{regularPlans.map(plan => (
<SimPlanCardCompact key={plan.id} plan={plan} onSelect={handleSelectPlan} />
))}
</div>
)}
{/* Family Discount Plans */}
{hasExistingSim && familyPlans.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-4">
<UsersIcon className="h-5 w-5 text-success" />
<h3 className="text-lg font-semibold text-foreground">Family Discount Plans</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
{familyPlans.map(plan => (
<SimPlanCardCompact
key={plan.id}
plan={plan}
isFamily
onSelect={handleSelectPlan}
/>
))}
</div>
</div>
)}
</div>
) : (
<div className="text-center py-16 text-muted-foreground">
No plans available in this category.
</div>
)}
</div>
{/* Collapsible Information Sections */}
<div className="mt-8 space-y-4">
{/* Calling & SMS Rates */}
<CollapsibleSection title="Calling & SMS Rates" icon={PhoneIcon}>
<div className="space-y-6 pt-4">
{/* Domestic Rates */}
<div>
<h4 className="text-sm font-medium text-foreground mb-3 flex items-center gap-2">
<span className="w-5 h-3 rounded-sm bg-[#BC002D] relative overflow-hidden flex items-center justify-center">
<span className="w-2 h-2 rounded-full bg-white" />
</span>
Domestic (Japan)
</h4>
<div className="grid grid-cols-2 gap-4">
<div className="bg-muted/50 rounded-lg p-4 border border-border">
<div className="text-sm text-muted-foreground mb-1">Voice Calls</div>
<div className="text-xl font-bold text-foreground">
¥10<span className="text-sm font-normal text-muted-foreground">/30 sec</span>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4 border border-border">
<div className="text-sm text-muted-foreground mb-1">SMS</div>
<div className="text-xl font-bold text-foreground">
¥3<span className="text-sm font-normal text-muted-foreground">/message</span>
</div>
</div>
</div>
<p className="text-xs text-muted-foreground mt-2">
Incoming calls and SMS are free. Pay-per-use charges billed 5-6 weeks after usage.
</p>
</div>
{/* Unlimited Option */}
<div className="p-4 bg-success/5 border border-success/20 rounded-lg">
<div className="flex items-start gap-3">
<PhoneIcon className="w-5 h-5 text-success mt-0.5" />
<div>
<h4 className="font-medium text-foreground">Unlimited Domestic Calling</h4>
<p className="text-sm text-muted-foreground">
Add unlimited domestic calls for{" "}
<span className="font-semibold text-success">¥3,000/month</span> (available at
checkout)
</p>
</div>
</div>
</div>
{/* International Note */}
<div className="text-sm text-muted-foreground">
<p>
International calling rates vary by country (¥31-148/30 sec). See{" "}
<a
href="https://www.docomo.ne.jp/service/world/worldcall/call/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
NTT Docomo's website
</a>{" "}
for full details.
</p>
</div>
</div>
</CollapsibleSection>
{/* Fees & Discounts */}
<CollapsibleSection title="Fees & Discounts" icon={BanknotesIcon}>
<div className="space-y-6 pt-4">
{/* Fees */}
<div>
<h4 className="text-sm font-medium text-foreground mb-3">One-time Fees</h4>
<div className="space-y-3">
<div className="flex items-center justify-between py-2 border-b border-border">
<span className="text-muted-foreground">Activation Fee</span>
<span className="font-medium text-foreground">¥1,500</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-border">
<span className="text-muted-foreground">SIM Replacement (lost/damaged)</span>
<span className="font-medium text-foreground">¥1,500</span>
</div>
<div className="flex items-center justify-between py-2">
<span className="text-muted-foreground">eSIM Re-download</span>
<span className="font-medium text-foreground">¥1,500</span>
</div>
</div>
</div>
{/* Discounts */}
<div className="p-4 bg-success/5 border border-success/20 rounded-lg">
<h4 className="font-medium text-foreground mb-2">Family Discount</h4>
<p className="text-sm text-muted-foreground">
<span className="font-semibold text-success">¥300/month off</span> per additional
Voice SIM on your account
</p>
</div>
<p className="text-xs text-muted-foreground">
All prices exclude 10% consumption tax.
</p>
</div>
</CollapsibleSection>
{/* Important Information & Terms */}
<CollapsibleSection title="Important Information & Terms" icon={InformationCircleIcon}>
<div className="space-y-6 pt-4 text-sm">
{/* Key Notices */}
<div>
<h4 className="font-medium text-foreground mb-3 flex items-center gap-2">
<ExclamationTriangleIcon className="w-4 h-4 text-warning" />
Important Notices
</h4>
<ul className="space-y-2 text-muted-foreground">
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
ID verification with official documents is required during checkout.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
A compatible unlocked device is required. Check compatibility on our website.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
SIM is activated as 4G by default. 5G can be requested via your account
portal.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
International data roaming is not available. Voice/SMS roaming can be enabled
upon request.
</span>
</li>
</ul>
</div>
{/* Contract Terms */}
<div>
<h4 className="font-medium text-foreground mb-3">Contract Terms</h4>
<ul className="space-y-2 text-muted-foreground">
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
<strong className="text-foreground">Minimum contract:</strong> 3 full billing
months.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
<strong className="text-foreground">Cancellation:</strong> Can be requested
after 3rd month via cancellation form.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<span>
<strong className="text-foreground">SIM return:</strong> SIM card must be
returned after service termination.
</span>
</li>
</ul>
</div>
{/* Disclaimer */}
<div className="p-4 bg-muted/50 rounded-lg">
<p className="text-xs text-muted-foreground">
Payment is by credit card only. Data service is not suitable for activities
requiring continuous large data transfers. See full Terms of Service for complete
details.
</p>
</div>
</div>
</CollapsibleSection>
</div>
{/* Terms Footer */}
<div className="mt-8 text-center text-sm text-muted-foreground">
<p>
All prices exclude 10% consumption tax.{" "}
<a href="#" className="text-primary hover:underline">
View full Terms of Service
</a>
</p>
</div>
</div>
</PageLayout>
);
}

View File

@ -9,10 +9,10 @@ import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { VpnPlanCard } from "@/features/catalog/components/vpn/VpnPlanCard";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { CatalogHero } from "@/features/catalog/components/base/CatalogHero";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
export function VpnPlansView() {
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
const { data, isLoading, error } = useVpnCatalog();
const vpnPlans = data?.plans || [];
const activationFees = data?.activationFees || [];
@ -26,7 +26,7 @@ export function VpnPlansView() {
icon={<ShieldCheckIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto px-4">
<CatalogBackLink href={shopBasePath} label="Back to Services" />
<CatalogBackLink href={servicesBasePath} label="Back to Services" />
<AsyncBlock
isLoading={isLoading}
@ -54,7 +54,7 @@ export function VpnPlansView() {
icon={<ShieldCheckIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto px-4 pb-16">
<CatalogBackLink href={shopBasePath} label="Back to Services" />
<CatalogBackLink href={servicesBasePath} label="Back to Services" />
<CatalogHero
title="SonixNet VPN Router Service"
@ -91,7 +91,7 @@ export function VpnPlansView() {
We couldn&apos;t find any VPN plans available at this time.
</p>
<CatalogBackLink
href={shopBasePath}
href={servicesBasePath}
label="Back to Services"
align="center"
className="mt-4 mb-0"

View File

@ -60,7 +60,6 @@ export function AccountCheckoutContainer() {
}, [cartItem?.orderType]);
const isInternetOrder = orderType === ORDER_TYPE.INTERNET;
const isSimOrder = orderType === ORDER_TYPE.SIM;
const { data: activeSubs } = useActiveSubscriptions();
const hasActiveInternetSubscription = useMemo(() => {
@ -85,7 +84,7 @@ export function AccountCheckoutContainer() {
const paymentRefresh = usePaymentRefresh({
refetch: refetchPaymentMethods,
attachFocusListeners: true,
attachFocusListeners: false,
});
const paymentMethodList = paymentMethods?.paymentMethods ?? [];
@ -136,14 +135,13 @@ export function AccountCheckoutContainer() {
.join(", ");
}, [user?.address]);
const residenceCardQuery = useResidenceCardVerification({ enabled: isSimOrder });
const residenceCardQuery = useResidenceCardVerification();
const submitResidenceCard = useSubmitResidenceCard();
const [residenceFile, setResidenceFile] = useState<File | null>(null);
const residenceFileInputRef = useRef<HTMLInputElement | null>(null);
const residenceStatus = residenceCardQuery.data?.status;
const residenceSubmitted =
!isSimOrder || residenceStatus === "pending" || residenceStatus === "verified";
const residenceSubmitted = residenceStatus === "pending" || residenceStatus === "verified";
const showPaymentToast = useCallback(
(text: string, tone: "info" | "success" | "warning" | "error") => {
@ -191,14 +189,14 @@ export function AccountCheckoutContainer() {
}
if (type === "sim") {
router.push(`/account/shop/sim/configure?${params.toString()}`);
router.push(`/account/services/sim/configure?${params.toString()}`);
return;
}
if (type === "internet" || type === "") {
router.push(`/account/shop/internet/configure?${params.toString()}`);
router.push(`/account/services/internet/configure?${params.toString()}`);
return;
}
router.push("/account/shop");
router.push("/account/services");
}, [router, searchParams]);
const handleSubmitOrder = useCallback(async () => {
@ -216,9 +214,8 @@ export function AccountCheckoutContainer() {
} catch (error) {
const message = error instanceof Error ? error.message : "Order submission failed";
if (
isSimOrder &&
(message.toLowerCase().includes("residence card submission required") ||
message.toLowerCase().includes("residence card submission was rejected"))
message.toLowerCase().includes("residence card submission required") ||
message.toLowerCase().includes("residence card submission was rejected")
) {
const next = `${pathname}${searchParams?.toString() ? `?${searchParams.toString()}` : ""}`;
router.push(`/account/settings/verification?returnTo=${encodeURIComponent(next)}`);
@ -228,7 +225,7 @@ export function AccountCheckoutContainer() {
} finally {
setSubmitting(false);
}
}, [checkoutSessionId, clear, isSimOrder, pathname, router, searchParams]);
}, [checkoutSessionId, clear, pathname, router, searchParams]);
const handleManagePayment = useCallback(async () => {
if (openingPaymentPortal) return;
@ -252,7 +249,7 @@ export function AccountCheckoutContainer() {
}, [openingPaymentPortal, showPaymentToast]);
if (!cartItem || !orderType) {
const shopHref = pathname.startsWith("/account") ? "/account/shop" : "/shop";
const shopHref = pathname.startsWith("/account") ? "/account/services" : "/services";
return (
<div className="max-w-2xl mx-auto py-8">
<AlertBanner variant="error" title="Checkout Error" elevated>
@ -411,340 +408,116 @@ export function AccountCheckoutContainer() {
)}
</SubCard>
{isSimOrder ? (
<SubCard
title="Identity verification"
icon={<ShieldCheckIcon className="w-5 h-5 text-primary" />}
right={
residenceStatus === "verified" ? (
<StatusPill label="Verified" variant="success" />
) : residenceStatus === "pending" ? (
<StatusPill label="Submitted" variant="info" />
) : residenceStatus === "rejected" ? (
<StatusPill label="Action needed" variant="warning" />
) : (
<StatusPill label="Required" variant="warning" />
)
}
>
{residenceCardQuery.isLoading ? (
<div className="text-sm text-muted-foreground">
Checking residence card status
</div>
) : residenceCardQuery.isError ? (
<AlertBanner
variant="warning"
title="Unable to load verification status"
size="sm"
elevated
>
<Button
type="button"
size="sm"
onClick={() => void residenceCardQuery.refetch()}
>
Check again
</Button>
</AlertBanner>
) : residenceStatus === "verified" ? (
<div className="space-y-3">
<AlertBanner
variant="success"
title="Residence card verified"
size="sm"
elevated
>
Your identity verification is complete.
</AlertBanner>
{residenceCardQuery.data?.filename || residenceCardQuery.data?.submittedAt ? (
<div className="rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Submitted document
</div>
{residenceCardQuery.data?.filename ? (
<div className="mt-1 text-sm font-medium text-foreground">
{residenceCardQuery.data.filename}
{typeof residenceCardQuery.data.sizeBytes === "number" &&
residenceCardQuery.data.sizeBytes > 0 ? (
<span className="text-xs text-muted-foreground">
{" "}
·{" "}
{Math.round(
(residenceCardQuery.data.sizeBytes / 1024 / 1024) * 10
) / 10}
{" MB"}
</span>
) : null}
</div>
) : null}
<div className="mt-1 text-xs text-muted-foreground space-y-0.5">
{formatDateTime(residenceCardQuery.data?.submittedAt) ? (
<div>
Submitted: {formatDateTime(residenceCardQuery.data?.submittedAt)}
</div>
) : null}
{formatDateTime(residenceCardQuery.data?.reviewedAt) ? (
<div>
Reviewed: {formatDateTime(residenceCardQuery.data?.reviewedAt)}
</div>
) : null}
</div>
</div>
) : null}
<details className="rounded-xl border border-border bg-card p-4">
<summary className="cursor-pointer select-none text-sm font-semibold text-foreground">
Replace residence card
</summary>
<div className="pt-3 space-y-3">
<p className="text-xs text-muted-foreground">
Replacing the file restarts the verification process.
</p>
<input
ref={residenceFileInputRef}
type="file"
accept="image/*,application/pdf"
onChange={e => setResidenceFile(e.target.files?.[0] ?? null)}
className="block w-full text-sm text-foreground file:mr-4 file:py-2 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-muted file:text-foreground hover:file:bg-muted/80"
/>
{residenceFile ? (
<div className="flex items-center justify-between gap-3 rounded-lg border border-border bg-muted/30 px-3 py-2">
<div className="min-w-0">
<div className="text-xs font-medium text-muted-foreground">
Selected file
</div>
<div className="text-sm font-medium text-foreground truncate">
{residenceFile.name}
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setResidenceFile(null);
if (residenceFileInputRef.current) {
residenceFileInputRef.current.value = "";
}
}}
>
Change
</Button>
</div>
) : null}
<div className="flex items-center justify-end">
<Button
type="button"
size="sm"
disabled={!residenceFile || submitResidenceCard.isPending}
isLoading={submitResidenceCard.isPending}
loadingText="Uploading…"
onClick={() => {
if (!residenceFile) return;
submitResidenceCard.mutate(residenceFile, {
onSuccess: () => {
setResidenceFile(null);
if (residenceFileInputRef.current) {
residenceFileInputRef.current.value = "";
}
},
});
}}
>
Submit replacement
</Button>
</div>
{submitResidenceCard.isError && (
<div className="text-sm text-destructive">
{submitResidenceCard.error instanceof Error
? submitResidenceCard.error.message
: "Failed to submit residence card."}
</div>
)}
</div>
</details>
</div>
<SubCard
title="Identity verification"
icon={<ShieldCheckIcon className="w-5 h-5 text-primary" />}
right={
residenceStatus === "verified" ? (
<StatusPill label="Verified" variant="success" />
) : residenceStatus === "pending" ? (
<div className="space-y-3">
<AlertBanner variant="info" title="Residence card submitted" size="sm" elevated>
Well verify your residence card before activating SIM service.
</AlertBanner>
<StatusPill label="Submitted" variant="info" />
) : residenceStatus === "rejected" ? (
<StatusPill label="Action needed" variant="warning" />
) : (
<StatusPill label="Required" variant="warning" />
)
}
>
{residenceCardQuery.isLoading ? (
<div className="text-sm text-muted-foreground">Checking residence card status</div>
) : residenceCardQuery.isError ? (
<AlertBanner
variant="warning"
title="Unable to load verification status"
size="sm"
elevated
>
<Button type="button" size="sm" onClick={() => void residenceCardQuery.refetch()}>
Check again
</Button>
</AlertBanner>
) : residenceStatus === "verified" ? (
<div className="space-y-3">
<AlertBanner variant="success" title="Residence card verified" size="sm" elevated>
Your identity verification is complete.
</AlertBanner>
{residenceCardQuery.data?.filename || residenceCardQuery.data?.submittedAt ? (
<div className="rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Submitted document
</div>
{residenceCardQuery.data?.filename ? (
<div className="mt-1 text-sm font-medium text-foreground">
{residenceCardQuery.data.filename}
{typeof residenceCardQuery.data.sizeBytes === "number" &&
residenceCardQuery.data.sizeBytes > 0 ? (
<span className="text-xs text-muted-foreground">
{" "}
·{" "}
{Math.round(
(residenceCardQuery.data.sizeBytes / 1024 / 1024) * 10
) / 10}
{" MB"}
</span>
) : null}
</div>
) : null}
<div className="mt-1 text-xs text-muted-foreground">
{formatDateTime(residenceCardQuery.data?.submittedAt) ? (
<div>
Submitted: {formatDateTime(residenceCardQuery.data?.submittedAt)}
</div>
{residenceCardQuery.data?.filename || residenceCardQuery.data?.submittedAt ? (
<div className="rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Submitted document
</div>
{residenceCardQuery.data?.filename ? (
<div className="mt-1 text-sm font-medium text-foreground">
{residenceCardQuery.data.filename}
{typeof residenceCardQuery.data.sizeBytes === "number" &&
residenceCardQuery.data.sizeBytes > 0 ? (
<span className="text-xs text-muted-foreground">
{" "}
·{" "}
{Math.round((residenceCardQuery.data.sizeBytes / 1024 / 1024) * 10) /
10}
{" MB"}
</span>
) : null}
</div>
</div>
) : null}
<details className="rounded-xl border border-border bg-card p-4">
<summary className="cursor-pointer select-none text-sm font-semibold text-foreground">
Replace residence card
</summary>
<div className="pt-3 space-y-3">
<p className="text-xs text-muted-foreground">
If you uploaded the wrong file, you can replace it. This restarts the
review.
</p>
<input
ref={residenceFileInputRef}
type="file"
accept="image/*,application/pdf"
onChange={e => setResidenceFile(e.target.files?.[0] ?? null)}
className="block w-full text-sm text-foreground file:mr-4 file:py-2 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-muted file:text-foreground hover:file:bg-muted/80"
/>
{residenceFile ? (
<div className="flex items-center justify-between gap-3 rounded-lg border border-border bg-muted/30 px-3 py-2">
<div className="min-w-0">
<div className="text-xs font-medium text-muted-foreground">
Selected file
</div>
<div className="text-sm font-medium text-foreground truncate">
{residenceFile.name}
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setResidenceFile(null);
if (residenceFileInputRef.current) {
residenceFileInputRef.current.value = "";
}
}}
>
Change
</Button>
) : null}
<div className="mt-1 text-xs text-muted-foreground space-y-0.5">
{formatDateTime(residenceCardQuery.data?.submittedAt) ? (
<div>
Submitted: {formatDateTime(residenceCardQuery.data?.submittedAt)}
</div>
) : null}
{formatDateTime(residenceCardQuery.data?.reviewedAt) ? (
<div>Reviewed: {formatDateTime(residenceCardQuery.data?.reviewedAt)}</div>
) : null}
</div>
</div>
) : null}
<div className="flex items-center justify-end">
<details className="rounded-xl border border-border bg-card p-4">
<summary className="cursor-pointer select-none text-sm font-semibold text-foreground">
Replace residence card
</summary>
<div className="pt-3 space-y-3">
<p className="text-xs text-muted-foreground">
Replacing the file restarts the verification process.
</p>
<input
ref={residenceFileInputRef}
type="file"
accept="image/*,application/pdf"
onChange={e => setResidenceFile(e.target.files?.[0] ?? null)}
className="block w-full text-sm text-foreground file:mr-4 file:py-2 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-muted file:text-foreground hover:file:bg-muted/80"
/>
{residenceFile ? (
<div className="flex items-center justify-between gap-3 rounded-lg border border-border bg-muted/30 px-3 py-2">
<div className="min-w-0">
<div className="text-xs font-medium text-muted-foreground">
Selected file
</div>
<div className="text-sm font-medium text-foreground truncate">
{residenceFile.name}
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
disabled={!residenceFile || submitResidenceCard.isPending}
isLoading={submitResidenceCard.isPending}
loadingText="Uploading…"
onClick={() => {
if (!residenceFile) return;
submitResidenceCard.mutate(residenceFile, {
onSuccess: () => {
setResidenceFile(null);
if (residenceFileInputRef.current) {
residenceFileInputRef.current.value = "";
}
},
});
setResidenceFile(null);
if (residenceFileInputRef.current) {
residenceFileInputRef.current.value = "";
}
}}
>
Submit replacement
Change
</Button>
</div>
{submitResidenceCard.isError && (
<div className="text-sm text-destructive">
{submitResidenceCard.error instanceof Error
? submitResidenceCard.error.message
: "Failed to submit residence card."}
</div>
)}
</div>
</details>
</div>
) : (
<AlertBanner
variant={residenceStatus === "rejected" ? "warning" : "info"}
title={
residenceStatus === "rejected"
? "ID verification rejected"
: "Submit your residence card"
}
size="sm"
elevated
>
<div className="space-y-3">
{residenceStatus === "rejected" && residenceCardQuery.data?.reviewerNotes ? (
<div className="text-sm text-foreground/80">
<div className="font-medium text-foreground">Rejection note</div>
<div>{residenceCardQuery.data.reviewerNotes}</div>
</div>
) : residenceStatus === "rejected" ? (
<p className="text-sm text-foreground/80">
Your document couldnt be approved. Please upload a new file to continue.
</p>
) : null}
<p className="text-sm text-foreground/80">
Upload a JPG, PNG, or PDF (max 5MB). Well verify it before activating SIM
service.
</p>
<div className="space-y-2">
<input
ref={residenceFileInputRef}
type="file"
accept="image/*,application/pdf"
onChange={e => setResidenceFile(e.target.files?.[0] ?? null)}
className="block w-full text-sm text-foreground file:mr-4 file:py-2 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-muted file:text-foreground hover:file:bg-muted/80"
/>
{residenceFile ? (
<div className="flex items-center justify-between gap-3 rounded-lg border border-border bg-muted/30 px-3 py-2">
<div className="min-w-0">
<div className="text-xs font-medium text-muted-foreground">
Selected file
</div>
<div className="text-sm font-medium text-foreground truncate">
{residenceFile.name}
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setResidenceFile(null);
if (residenceFileInputRef.current) {
residenceFileInputRef.current.value = "";
}
}}
>
Change
</Button>
</div>
) : null}
</div>
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<div className="flex items-center justify-end">
<Button
type="button"
size="sm"
@ -762,9 +535,8 @@ export function AccountCheckoutContainer() {
},
});
}}
className="sm:ml-auto whitespace-nowrap"
>
Submit for review
Submit replacement
</Button>
</div>
@ -776,10 +548,218 @@ export function AccountCheckoutContainer() {
</div>
)}
</div>
</details>
</div>
) : residenceStatus === "pending" ? (
<div className="space-y-3">
<AlertBanner variant="info" title="Residence card submitted" size="sm" elevated>
Well verify your residence card before activating SIM service.
</AlertBanner>
)}
</SubCard>
) : null}
{residenceCardQuery.data?.filename || residenceCardQuery.data?.submittedAt ? (
<div className="rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Submitted document
</div>
{residenceCardQuery.data?.filename ? (
<div className="mt-1 text-sm font-medium text-foreground">
{residenceCardQuery.data.filename}
{typeof residenceCardQuery.data.sizeBytes === "number" &&
residenceCardQuery.data.sizeBytes > 0 ? (
<span className="text-xs text-muted-foreground">
{" "}
·{" "}
{Math.round((residenceCardQuery.data.sizeBytes / 1024 / 1024) * 10) /
10}
{" MB"}
</span>
) : null}
</div>
) : null}
<div className="mt-1 text-xs text-muted-foreground">
{formatDateTime(residenceCardQuery.data?.submittedAt) ? (
<div>
Submitted: {formatDateTime(residenceCardQuery.data?.submittedAt)}
</div>
) : null}
</div>
</div>
) : null}
<details className="rounded-xl border border-border bg-card p-4">
<summary className="cursor-pointer select-none text-sm font-semibold text-foreground">
Replace residence card
</summary>
<div className="pt-3 space-y-3">
<p className="text-xs text-muted-foreground">
If you uploaded the wrong file, you can replace it. This restarts the
review.
</p>
<input
ref={residenceFileInputRef}
type="file"
accept="image/*,application/pdf"
onChange={e => setResidenceFile(e.target.files?.[0] ?? null)}
className="block w-full text-sm text-foreground file:mr-4 file:py-2 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-muted file:text-foreground hover:file:bg-muted/80"
/>
{residenceFile ? (
<div className="flex items-center justify-between gap-3 rounded-lg border border-border bg-muted/30 px-3 py-2">
<div className="min-w-0">
<div className="text-xs font-medium text-muted-foreground">
Selected file
</div>
<div className="text-sm font-medium text-foreground truncate">
{residenceFile.name}
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setResidenceFile(null);
if (residenceFileInputRef.current) {
residenceFileInputRef.current.value = "";
}
}}
>
Change
</Button>
</div>
) : null}
<div className="flex items-center justify-end">
<Button
type="button"
size="sm"
disabled={!residenceFile || submitResidenceCard.isPending}
isLoading={submitResidenceCard.isPending}
loadingText="Uploading…"
onClick={() => {
if (!residenceFile) return;
submitResidenceCard.mutate(residenceFile, {
onSuccess: () => {
setResidenceFile(null);
if (residenceFileInputRef.current) {
residenceFileInputRef.current.value = "";
}
},
});
}}
>
Submit replacement
</Button>
</div>
{submitResidenceCard.isError && (
<div className="text-sm text-destructive">
{submitResidenceCard.error instanceof Error
? submitResidenceCard.error.message
: "Failed to submit residence card."}
</div>
)}
</div>
</details>
</div>
) : (
<AlertBanner
variant={residenceStatus === "rejected" ? "warning" : "info"}
title={
residenceStatus === "rejected"
? "ID verification rejected"
: "Submit your residence card"
}
size="sm"
elevated
>
<div className="space-y-3">
{residenceStatus === "rejected" && residenceCardQuery.data?.reviewerNotes ? (
<div className="text-sm text-foreground/80">
<div className="font-medium text-foreground">Rejection note</div>
<div>{residenceCardQuery.data.reviewerNotes}</div>
</div>
) : residenceStatus === "rejected" ? (
<p className="text-sm text-foreground/80">
Your document couldnt be approved. Please upload a new file to continue.
</p>
) : null}
<p className="text-sm text-foreground/80">
Upload a JPG, PNG, or PDF (max 5MB). Well verify it before activating SIM
service.
</p>
<div className="space-y-2">
<input
ref={residenceFileInputRef}
type="file"
accept="image/*,application/pdf"
onChange={e => setResidenceFile(e.target.files?.[0] ?? null)}
className="block w-full text-sm text-foreground file:mr-4 file:py-2 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-muted file:text-foreground hover:file:bg-muted/80"
/>
{residenceFile ? (
<div className="flex items-center justify-between gap-3 rounded-lg border border-border bg-muted/30 px-3 py-2">
<div className="min-w-0">
<div className="text-xs font-medium text-muted-foreground">
Selected file
</div>
<div className="text-sm font-medium text-foreground truncate">
{residenceFile.name}
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setResidenceFile(null);
if (residenceFileInputRef.current) {
residenceFileInputRef.current.value = "";
}
}}
>
Change
</Button>
</div>
) : null}
</div>
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<Button
type="button"
size="sm"
disabled={!residenceFile || submitResidenceCard.isPending}
isLoading={submitResidenceCard.isPending}
loadingText="Uploading…"
onClick={() => {
if (!residenceFile) return;
submitResidenceCard.mutate(residenceFile, {
onSuccess: () => {
setResidenceFile(null);
if (residenceFileInputRef.current) {
residenceFileInputRef.current.value = "";
}
},
});
}}
className="sm:ml-auto whitespace-nowrap"
>
Submit for review
</Button>
</div>
{submitResidenceCard.isError && (
<div className="text-sm text-destructive">
{submitResidenceCard.error instanceof Error
? submitResidenceCard.error.message
: "Failed to submit residence card."}
</div>
)}
</div>
</AlertBanner>
)}
</SubCard>
</div>
</div>
@ -806,9 +786,7 @@ export function AccountCheckoutContainer() {
<div className="text-sm text-muted-foreground space-y-1">
<p> Our team reviews your order and schedules setup if needed</p>
<p> We may contact you to confirm details or availability</p>
{isSimOrder ? (
<p> For SIM orders, we verify your residence card before SIM activation</p>
) : null}
<p> We verify your residence card before service activation</p>
<p> We only charge your card after the order is approved</p>
<p> Youll receive confirmation and next steps by email</p>
</div>

View File

@ -187,7 +187,7 @@ export function CheckoutEntry() {
}
if (status === "error") {
const shopHref = pathname.startsWith("/account") ? "/account/shop" : "/shop";
const shopHref = pathname.startsWith("/account") ? "/account/services" : "/services";
return (
<div className="max-w-2xl mx-auto py-8">
<AlertBanner variant="error" title="Unable to start checkout" elevated>

View File

@ -52,7 +52,7 @@ export class CheckoutErrorBoundary extends Component<Props, State> {
>
Try Again
</Button>
<Button as="a" href="/shop">
<Button as="a" href="/services">
Return to Catalog
</Button>
</div>

View File

@ -68,7 +68,7 @@ export function CheckoutStatusBanners({
Were verifying whether our service is available at your residence. Once eligibility
is confirmed, you can submit your internet order.
</span>
<Button as="a" href="/account/shop/internet" size="sm" className="sm:ml-auto">
<Button as="a" href="/account/services/internet" size="sm" className="sm:ml-auto">
View status
</Button>
</div>

View File

@ -4,7 +4,7 @@ import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { ShoppingCartIcon } from "@heroicons/react/24/outline";
import { Button } from "@/components/atoms/button";
import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
import { useServicesBasePath } from "@/features/catalog/hooks/useServicesBasePath";
/**
* EmptyCartRedirect - Shown when checkout is accessed without a cart
@ -13,15 +13,15 @@ import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath";
*/
export function EmptyCartRedirect() {
const router = useRouter();
const shopBasePath = useShopBasePath();
const servicesBasePath = useServicesBasePath();
useEffect(() => {
const timer = setTimeout(() => {
router.push(shopBasePath);
router.push(servicesBasePath);
}, 5000);
return () => clearTimeout(timer);
}, [router, shopBasePath]);
}, [router, servicesBasePath]);
return (
<div className="max-w-md mx-auto text-center py-16">
@ -33,7 +33,7 @@ export function EmptyCartRedirect() {
<p className="text-muted-foreground mb-6">
Browse our services to find the perfect plan for your needs.
</p>
<Button as="a" href={shopBasePath} className="w-full">
<Button as="a" href={servicesBasePath} className="w-full">
Browse Services
</Button>
<p className="text-xs text-muted-foreground mt-4">

View File

@ -52,7 +52,7 @@ function AllCaughtUp() {
{/* Quick action cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-8 max-w-lg mx-auto">
<Link
href="/shop"
href="/account/services"
className="group flex flex-col items-center gap-2 p-4 rounded-xl bg-surface/80 backdrop-blur-sm border border-border/60 hover:border-primary/40 hover:shadow-lg transition-all"
>
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">

View File

@ -1,218 +1,212 @@
import Link from "next/link";
import { Logo } from "@/components/atoms/logo";
import {
UserIcon,
SparklesIcon,
CreditCardIcon,
Cog6ToothIcon,
PhoneIcon,
ArrowRightIcon,
ShoppingBagIcon,
ServerIcon,
DevicePhoneMobileIcon,
ShieldCheckIcon,
CheckBadgeIcon,
GlobeAltIcon,
WrenchScrewdriverIcon,
BuildingOfficeIcon,
TvIcon,
} from "@heroicons/react/24/outline";
/**
* PublicLandingView - Marketing-focused landing page
*
* Purpose: Hook visitors, build trust, guide to shop
* Contains:
* - Hero with tagline
* - Value props (One Stop Solution, English Support, Onsite Support) - ONLY here
* - Brief service tease (links to /services)
* - CTA to contact
*/
export function PublicLandingView() {
return (
<div className="space-y-12">
{/* Hero */}
<section className="text-center space-y-8">
<div className="relative inline-block">
{/* Subtle glow behind logo */}
<div className="absolute inset-0 bg-primary/15 blur-3xl rounded-full scale-[2]" />
<div className="relative inline-flex items-center justify-center h-24 w-24 rounded-3xl bg-card border border-border/40 shadow-2xl shadow-primary/20 mx-auto">
<Logo size={56} />
<div className="space-y-20 pb-8">
{/* Hero Section */}
<section className="text-center space-y-8 pt-8 sm:pt-16 relative">
<div className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-primary/5 via-transparent to-transparent opacity-70 blur-3xl pointer-events-none" />
<div className="space-y-6 max-w-4xl mx-auto px-4">
<div className="inline-flex items-center gap-2 rounded-full bg-primary/5 border border-primary/10 px-3 py-1 text-sm text-primary font-medium mb-4 animate-in fade-in slide-in-from-bottom-4 duration-500">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
</span>
Reliable Connectivity in Japan
</div>
</div>
<div className="space-y-4">
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight text-foreground">
Account Portal
<h1 className="text-4xl sm:text-5xl lg:text-7xl font-extrabold tracking-tight text-foreground animate-in fade-in slide-in-from-bottom-6 duration-700">
A One Stop Solution
<span className="block bg-gradient-to-r from-primary to-blue-600 bg-clip-text text-transparent pb-2 mt-2">
for Your IT Needs
</span>
</h1>
<p className="text-lg sm:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
Manage your services, billing, and support in one place.
<p className="text-lg sm:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed animate-in fade-in slide-in-from-bottom-8 duration-700 delay-100">
Serving Japan&apos;s international community with reliable, English-supported internet,
mobile, and VPN solutions.
</p>
</div>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4 animate-in fade-in slide-in-from-bottom-8 duration-700 delay-200">
<Link
href="/services"
className="inline-flex items-center justify-center gap-2 rounded-lg px-8 py-4 text-lg font-semibold bg-primary text-primary-foreground hover:bg-primary/90 shadow-lg shadow-primary/20 hover:shadow-primary/30 transition-all hover:-translate-y-0.5"
>
Browse Services
<ArrowRightIcon className="h-5 w-5" />
</Link>
<Link
href="/contact"
className="inline-flex items-center justify-center gap-2 rounded-lg px-8 py-4 text-lg font-semibold border border-input bg-background hover:bg-accent hover:text-accent-foreground transition-all hover:-translate-y-0.5"
>
Contact Us
</Link>
</div>
</section>
{/* Browse services CTA - New prominent section */}
<section className="max-w-4xl mx-auto">
<div className="group relative bg-gradient-to-br from-primary/10 via-primary/5 to-transparent rounded-2xl border border-primary/20 p-8 shadow-lg shadow-primary/10 hover:shadow-xl hover:shadow-primary/15 transition-all duration-300">
<div className="flex flex-col sm:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-5">
<div className="h-16 w-16 rounded-2xl bg-gradient-to-br from-primary/20 to-primary/10 border border-primary/25 flex items-center justify-center flex-shrink-0">
<ShoppingBagIcon className="h-8 w-8 text-primary" />
</div>
<div>
<h2 className="text-2xl font-bold text-foreground">Browse Our Services</h2>
<p className="text-muted-foreground mt-1">
Explore internet, SIM, and VPN plans no account needed
</p>
</div>
{/* CONCEPT Section - Value Propositions (ONLY on homepage) */}
<section className="max-w-5xl mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-sm font-semibold text-primary uppercase tracking-wider mb-3">
Our Concept
</h2>
<p className="text-3xl font-bold text-foreground tracking-tight">
Why customers choose us
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* One Stop Solution */}
<div className="flex flex-col items-center text-center">
<div className="h-14 w-14 rounded-xl bg-primary/10 flex items-center justify-center mb-6 text-primary">
<CheckBadgeIcon className="h-7 w-7" />
</div>
<Link
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"
>
Shop Services
<ArrowRightIcon className="h-5 w-5" />
</Link>
<h3 className="text-xl font-bold text-foreground mb-3">One Stop Solution</h3>
<p className="text-muted-foreground leading-relaxed max-w-xs">
All you need is just to contact us and we will take care of everything.
</p>
</div>
{/* English Support */}
<div className="flex flex-col items-center text-center">
<div className="h-14 w-14 rounded-xl bg-primary/10 flex items-center justify-center mb-6 text-primary">
<GlobeAltIcon className="h-7 w-7" />
</div>
<h3 className="text-xl font-bold text-foreground mb-3">English Support</h3>
<p className="text-muted-foreground leading-relaxed max-w-xs">
We always assist you in English. No language barrier to worry about.
</p>
</div>
{/* Onsite Support */}
<div className="flex flex-col items-center text-center">
<div className="h-14 w-14 rounded-xl bg-primary/10 flex items-center justify-center mb-6 text-primary">
<WrenchScrewdriverIcon className="h-7 w-7" />
</div>
<h3 className="text-xl font-bold text-foreground mb-3">Onsite Support</h3>
<p className="text-muted-foreground leading-relaxed max-w-xs">
Our tech staff can visit your residence for setup and troubleshooting.
</p>
</div>
</div>
</section>
{/* Service highlights */}
<section className="max-w-4xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Services Teaser - Brief preview linking to /services */}
<section className="max-w-5xl mx-auto px-4 text-center">
<h2 className="text-sm font-semibold text-primary uppercase tracking-wider mb-3">
Our Services
</h2>
<p className="text-3xl font-bold text-foreground tracking-tight mb-8">What we offer</p>
<div className="flex flex-wrap justify-center gap-4 mb-8">
<Link
href="/shop/internet"
className="group rounded-2xl border border-border/50 bg-card p-6 hover:border-blue-500/30 hover:shadow-lg transition-all duration-300 hover:-translate-y-1"
href="/services/internet"
className="group inline-flex items-center gap-3 rounded-full border border-border bg-card px-5 py-3 hover:border-blue-500/50 hover:bg-blue-500/5 transition-all"
>
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-blue-500/15 to-blue-500/5 border border-blue-500/15 flex items-center justify-center mb-4">
<ServerIcon className="h-6 w-6 text-blue-500" />
</div>
<div className="text-lg font-semibold text-foreground group-hover:text-blue-500 transition-colors">
<ServerIcon className="h-5 w-5 text-blue-500" />
<span className="font-medium text-foreground group-hover:text-blue-500 transition-colors">
Internet
</div>
<div className="text-sm text-muted-foreground mt-1">Up to 10Gbps fiber</div>
</span>
</Link>
<Link
href="/shop/sim"
className="group rounded-2xl border border-border/50 bg-card p-6 hover:border-green-500/30 hover:shadow-lg transition-all duration-300 hover:-translate-y-1"
href="/services/sim"
className="group inline-flex items-center gap-3 rounded-full border border-border bg-card px-5 py-3 hover:border-green-500/50 hover:bg-green-500/5 transition-all"
>
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-green-500/15 to-green-500/5 border border-green-500/15 flex items-center justify-center mb-4">
<DevicePhoneMobileIcon className="h-6 w-6 text-green-500" />
</div>
<div className="text-lg font-semibold text-foreground group-hover:text-green-500 transition-colors">
<DevicePhoneMobileIcon className="h-5 w-5 text-green-500" />
<span className="font-medium text-foreground group-hover:text-green-500 transition-colors">
SIM & eSIM
</div>
<div className="text-sm text-muted-foreground mt-1">Data, voice & SMS plans</div>
</span>
</Link>
<Link
href="/shop/vpn"
className="group rounded-2xl border border-border/50 bg-card p-6 hover:border-purple-500/30 hover:shadow-lg transition-all duration-300 hover:-translate-y-1"
href="/services/vpn"
className="group inline-flex items-center gap-3 rounded-full border border-border bg-card px-5 py-3 hover:border-purple-500/50 hover:bg-purple-500/5 transition-all"
>
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-purple-500/15 to-purple-500/5 border border-purple-500/15 flex items-center justify-center mb-4">
<ShieldCheckIcon className="h-6 w-6 text-purple-500" />
</div>
<div className="text-lg font-semibold text-foreground group-hover:text-purple-500 transition-colors">
<ShieldCheckIcon className="h-5 w-5 text-purple-500" />
<span className="font-medium text-foreground group-hover:text-purple-500 transition-colors">
VPN
</div>
<div className="text-sm text-muted-foreground mt-1">Secure remote access</div>
</span>
</Link>
<Link
href="/services/business"
className="group inline-flex items-center gap-3 rounded-full border border-border bg-card px-5 py-3 hover:border-amber-500/50 hover:bg-amber-500/5 transition-all"
>
<BuildingOfficeIcon className="h-5 w-5 text-amber-500" />
<span className="font-medium text-foreground group-hover:text-amber-500 transition-colors">
Business
</span>
</Link>
<Link
href="/services/onsite"
className="group inline-flex items-center gap-3 rounded-full border border-border bg-card px-5 py-3 hover:border-rose-500/50 hover:bg-rose-500/5 transition-all"
>
<WrenchScrewdriverIcon className="h-5 w-5 text-rose-500" />
<span className="font-medium text-foreground group-hover:text-rose-500 transition-colors">
Onsite Support
</span>
</Link>
<Link
href="/services/tv"
className="group inline-flex items-center gap-3 rounded-full border border-border bg-card px-5 py-3 hover:border-cyan-500/50 hover:bg-cyan-500/5 transition-all"
>
<TvIcon className="h-5 w-5 text-cyan-500" />
<span className="font-medium text-foreground group-hover:text-cyan-500 transition-colors">
TV Services
</span>
</Link>
</div>
<Link
href="/services"
className="inline-flex items-center gap-2 text-primary font-semibold hover:text-primary/80 transition-colors"
>
Browse all services
<ArrowRightIcon className="h-4 w-4" />
</Link>
</section>
{/* Primary actions */}
<section className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
<div className="group relative bg-card rounded-2xl border border-border/50 p-7 shadow-lg shadow-black/5 hover:shadow-xl hover:border-border/80 transition-all duration-300 hover:-translate-y-1">
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative flex items-start gap-5">
<div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-primary/15 to-primary/5 border border-primary/20 flex items-center justify-center flex-shrink-0">
<UserIcon className="h-7 w-7 text-primary" />
</div>
<div className="min-w-0 flex-1">
<h2 className="text-xl font-semibold text-foreground">Existing customers</h2>
<p className="text-sm text-muted-foreground mt-2 leading-relaxed">
Sign in or migrate your account from the old system.
</p>
<div className="mt-6 flex flex-col sm:flex-row gap-3">
<Link
href="/auth/login"
className="inline-flex items-center justify-center gap-2 rounded-xl px-6 py-3 text-sm font-semibold bg-primary text-primary-foreground hover:bg-primary-hover shadow-md shadow-primary/25 hover:shadow-lg hover:shadow-primary/30 transition-all"
>
Sign in
<ArrowRightIcon className="h-4 w-4" />
</Link>
<Link
href="/auth/migrate"
className="inline-flex items-center justify-center rounded-xl px-6 py-3 text-sm font-medium border border-border bg-card hover:bg-muted/50 transition-colors"
>
Migrate account
</Link>
</div>
</div>
</div>
</div>
<div className="group relative bg-card rounded-2xl border border-border/50 p-7 shadow-lg shadow-black/5 hover:shadow-xl hover:border-border/80 transition-all duration-300 hover:-translate-y-1">
<div className="absolute inset-0 bg-gradient-to-br from-success/5 to-transparent rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative flex items-start gap-5">
<div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-success/15 to-success/5 border border-success/20 flex items-center justify-center flex-shrink-0">
<SparklesIcon className="h-7 w-7 text-success" />
</div>
<div className="min-w-0 flex-1">
<h2 className="text-xl font-semibold text-foreground">New customers</h2>
<p className="text-sm text-muted-foreground mt-2 leading-relaxed">
Browse our services and sign up during checkout, or create an account first.
</p>
<div className="mt-6 flex flex-col sm:flex-row gap-3">
<Link
href="/shop"
className="inline-flex items-center justify-center gap-2 rounded-xl px-6 py-3 text-sm font-semibold bg-primary text-primary-foreground hover:bg-primary-hover shadow-md shadow-primary/25 hover:shadow-lg hover:shadow-primary/30 transition-all"
>
Browse services
<ArrowRightIcon className="h-4 w-4" />
</Link>
<Link
href="/auth/signup"
className="inline-flex items-center justify-center rounded-xl px-6 py-3 text-sm font-medium border border-border bg-card hover:bg-muted/50 transition-colors"
>
Create account
</Link>
</div>
</div>
</div>
</div>
</section>
{/* Feature highlights */}
<section className="max-w-4xl mx-auto">
<div className="bg-card rounded-2xl border border-border/50 p-8 sm:p-10 shadow-lg shadow-black/5">
<div className="flex items-center justify-between gap-4 flex-wrap mb-10">
<div>
<h2 className="text-2xl font-bold text-foreground">Everything you need</h2>
<p className="text-muted-foreground mt-2">Powerful tools to manage your account</p>
</div>
<Link
href="/help"
className="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:text-primary-hover transition-colors"
>
Need help?
<ArrowRightIcon className="h-4 w-4" />
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
<div className="group rounded-2xl border border-border/50 bg-card p-6 hover:border-primary/30 hover:shadow-md transition-all duration-300">
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-primary/15 to-primary/5 border border-primary/15 flex items-center justify-center mb-5">
<CreditCardIcon className="h-6 w-6 text-primary" />
</div>
<div className="text-lg font-semibold text-foreground">Billing</div>
<div className="text-sm text-muted-foreground mt-2 leading-relaxed">
View invoices, payments, and billing history.
</div>
</div>
<div className="group rounded-2xl border border-border/50 bg-card p-6 hover:border-info/30 hover:shadow-md transition-all duration-300">
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-info/15 to-info/5 border border-info/15 flex items-center justify-center mb-5">
<Cog6ToothIcon className="h-6 w-6 text-info" />
</div>
<div className="text-lg font-semibold text-foreground">Services</div>
<div className="text-sm text-muted-foreground mt-2 leading-relaxed">
Manage subscriptions and service details.
</div>
</div>
<div className="group rounded-2xl border border-border/50 bg-card p-6 hover:border-success/30 hover:shadow-md transition-all duration-300">
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-success/15 to-success/5 border border-success/15 flex items-center justify-center mb-5">
<PhoneIcon className="h-6 w-6 text-success" />
</div>
<div className="text-lg font-semibold text-foreground">Support</div>
<div className="text-sm text-muted-foreground mt-2 leading-relaxed">
Create cases and track responses in one place.
</div>
{/* CTA Section */}
<section className="max-w-3xl mx-auto text-center px-4">
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-primary/5 via-primary/10 to-blue-600/5 border border-primary/20 p-8 sm:p-12">
<div className="absolute top-0 right-0 -translate-y-1/4 translate-x-1/4 w-64 h-64 bg-primary/10 rounded-full blur-3xl pointer-events-none" />
<div className="absolute bottom-0 left-0 translate-y-1/4 -translate-x-1/4 w-48 h-48 bg-blue-500/10 rounded-full blur-3xl pointer-events-none" />
<div className="relative">
<h2 className="text-2xl sm:text-3xl font-bold text-foreground mb-4">
Ready to get connected?
</h2>
<p className="text-muted-foreground max-w-lg mx-auto mb-8 leading-relaxed">
Contact us anytime our bilingual team is here to help you find the right solution.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href="/contact"
className="inline-flex items-center justify-center gap-2 rounded-lg px-8 py-3 text-base font-semibold bg-primary text-primary-foreground hover:bg-primary/90 shadow-lg shadow-primary/20 transition-all"
>
Contact Us
<ArrowRightIcon className="h-4 w-4" />
</Link>
<Link
href="/services"
className="inline-flex items-center justify-center gap-2 rounded-lg px-8 py-3 text-base font-semibold border border-input bg-background hover:bg-accent hover:text-accent-foreground transition-colors"
>
Browse Services
</Link>
</div>
</div>
</div>

View File

@ -90,7 +90,7 @@ export function OrdersListContainer() {
icon={<ClipboardDocumentListIcon className="h-12 w-12" />}
title="No orders yet"
description="You haven't placed any orders yet."
action={{ label: "Browse Services", onClick: () => router.push("/shop") }}
action={{ label: "Browse Services", onClick: () => router.push("/account/services") }}
/>
</AnimatedCard>
) : (

View File

@ -94,7 +94,7 @@ export function SubscriptionsListContainer() {
title="Services"
description="Manage your active services"
actions={
<Button as="a" href="/shop" size="sm">
<Button as="a" href="/account/services" size="sm">
Shop Services
</Button>
}

View File

@ -7,7 +7,7 @@ import { Button, Input } from "@/components/atoms";
import { FormField } from "@/components/molecules/FormField/FormField";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { useZodForm } from "@/hooks/useZodForm";
import { EnvelopeIcon, ArrowLeftIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
import { EnvelopeIcon, CheckCircleIcon, MapPinIcon } from "@heroicons/react/24/outline";
const contactFormSchema = z.object({
name: z.string().min(1, "Name is required"),
@ -20,7 +20,7 @@ const contactFormSchema = z.object({
type ContactFormData = z.infer<typeof contactFormSchema>;
/**
* PublicContactView - Contact form for unauthenticated users
* PublicContactView - Contact page with form, phone, chat, and location info
*/
export function PublicContactView() {
const [isSubmitted, setIsSubmitted] = useState(false);
@ -67,14 +67,14 @@ export function PublicContactView() {
</div>
<h1 className="text-2xl font-bold text-foreground mb-2">Message Sent!</h1>
<p className="text-muted-foreground mb-6">
Thank you for contacting us. We'll get back to you within 24 hours.
Thank you for contacting us. We&apos;ll get back to you within 24 hours.
</p>
<div className="flex gap-4 justify-center">
<Button as="a" href="/help" variant="outline">
Back to Support
</Button>
<Button as="a" href="/shop">
Browse Catalog
<Button as="a" href="/services">
Browse Services
</Button>
</div>
</div>
@ -82,117 +82,224 @@ export function PublicContactView() {
}
return (
<div className="max-w-lg mx-auto">
{/* Back link */}
<Link
href="/help"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-6 transition-colors"
>
<ArrowLeftIcon className="h-4 w-4" />
Back to Support
</Link>
<div className="max-w-6xl mx-auto px-4">
{/* Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-14 h-14 bg-primary/10 rounded-full mb-4">
<EnvelopeIcon className="h-7 w-7 text-primary" />
<div className="text-center mb-16 pt-8">
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-6 tracking-tight">
Get in Touch
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
Have a question about our services? We're here to help you find the perfect solution for
your stay in Japan.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10">
{/* Left Column - Contact Form */}
<div className="lg:col-span-7">
<div className="bg-card rounded-2xl border border-border/60 shadow-sm overflow-hidden">
<div className="p-6 sm:p-8 border-b border-border/60 bg-muted/20">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary">
<EnvelopeIcon className="h-5 w-5" />
</div>
<div>
<h2 className="text-xl font-bold text-foreground">Send a Message</h2>
<p className="text-sm text-muted-foreground">
We typically reply within 24 hours
</p>
</div>
</div>
</div>
<div className="p-6 sm:p-8">
{submitError && (
<AlertBanner variant="error" title="Error" className="mb-6">
{submitError}
</AlertBanner>
)}
<form onSubmit={event => void form.handleSubmit(event)} className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<FormField
label="Name"
error={form.touched.name ? form.errors.name : undefined}
required
>
<Input
value={form.values.name}
onChange={e => form.setValue("name", e.target.value)}
onBlur={() => form.setTouchedField("name")}
placeholder="Your name"
className="bg-background"
/>
</FormField>
<FormField
label="Email"
error={form.touched.email ? form.errors.email : undefined}
required
>
<Input
type="email"
value={form.values.email}
onChange={e => form.setValue("email", e.target.value)}
onBlur={() => form.setTouchedField("email")}
placeholder="your@email.com"
className="bg-background"
/>
</FormField>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<FormField
label="Phone (Optional)"
error={form.touched.phone ? form.errors.phone : undefined}
>
<Input
value={form.values.phone ?? ""}
onChange={e => form.setValue("phone", e.target.value)}
onBlur={() => form.setTouchedField("phone")}
placeholder="+81 90-1234-5678"
className="bg-background"
/>
</FormField>
<FormField
label="Subject"
error={form.touched.subject ? form.errors.subject : undefined}
required
>
<Input
value={form.values.subject}
onChange={e => form.setValue("subject", e.target.value)}
onBlur={() => form.setTouchedField("subject")}
placeholder="How can we help?"
className="bg-background"
/>
</FormField>
</div>
<FormField
label="Message"
error={form.touched.message ? form.errors.message : undefined}
required
>
<textarea
className="flex min-h-[160px] w-full rounded-lg border border-input bg-background px-4 py-3 text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:border-transparent disabled:cursor-not-allowed disabled:opacity-50 transition-colors resize-y"
value={form.values.message}
onChange={e => form.setValue("message", e.target.value)}
onBlur={() => form.setTouchedField("message")}
placeholder="Tell us more about your inquiry..."
rows={5}
/>
</FormField>
<Button
type="submit"
className="w-full sm:w-auto min-w-[160px]"
size="lg"
disabled={form.isSubmitting}
isLoading={form.isSubmitting}
loadingText="Sending..."
>
Send Message
</Button>
</form>
<p className="text-xs text-muted-foreground mt-6 pt-6 border-t border-border/60">
By submitting this form, you agree to our{" "}
<Link href="#" className="text-primary hover:underline font-medium">
Privacy Policy
</Link>
. Your information is secure and will only be used to respond to your inquiry.
</p>
</div>
</div>
</div>
{/* Right Column - Contact Info */}
<div className="lg:col-span-5 space-y-6">
{/* By Phone */}
<div className="bg-card rounded-2xl border border-border/60 p-6 sm:p-8 hover:border-primary/30 transition-colors duration-300">
<h2 className="text-xl font-bold text-foreground mb-4">Phone Support</h2>
<div className="space-y-4">
<div>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1">
Japan (Toll Free)
</div>
<a
href="tel:0120-660-470"
className="text-2xl font-bold text-foreground hover:text-primary transition-colors inline-block"
>
0120-660-470
</a>
</div>
<div>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1">
International
</div>
<a
href="tel:+81-3-3560-1006"
className="text-lg font-semibold text-foreground hover:text-primary transition-colors inline-block"
>
+81-3-3560-1006
</a>
</div>
<div className="text-sm text-muted-foreground pt-4 border-t border-border/60">
9:30 - 18:00 JST (Mon - Fri)
</div>
</div>
</div>
{/* By Chat */}
<div className="bg-card rounded-2xl border border-border/60 p-6 sm:p-8 hover:border-blue-500/30 transition-colors duration-300">
<h2 className="text-xl font-bold text-foreground mb-4">Live Chat</h2>
<p className="text-muted-foreground mb-6">
Need quick answers? Chat with our support team directly in your browser.
</p>
<Button
variant="outline"
className="w-full justify-start"
onClick={() => {
/* Trigger chat logic would go here */
}}
>
<span className="flex items-center gap-2">
<span className="relative flex h-2 w-2 mr-1">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-success"></span>
</span>
Chat Available
</span>
</Button>
</div>
{/* Access / Location */}
<div className="bg-card rounded-2xl border border-border/60 p-6 sm:p-8 hover:border-primary/30 transition-colors duration-300">
<div className="flex items-center gap-3 mb-6">
<div className="h-10 w-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary">
<MapPinIcon className="h-5 w-5" />
</div>
<h2 className="text-xl font-bold text-foreground">Visit Us</h2>
</div>
<div className="space-y-4">
<address className="text-muted-foreground leading-relaxed not-italic">
3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu,
<br />
Minato-ku, Tokyo 106-0044
</address>
<div className="pt-4 border-t border-border/60">
<p className="text-sm text-muted-foreground">
Short walk from Exit 6 of Azabu-Juban Station
<br />
(Subway Oedo Line / Nanboku Line)
</p>
</div>
</div>
</div>
</div>
<h1 className="text-2xl font-bold text-foreground mb-2">Contact Us</h1>
<p className="text-muted-foreground">Have a question? We'd love to hear from you.</p>
</div>
{/* Form */}
<div className="bg-card rounded-xl border border-border p-6 shadow-[var(--cp-shadow-1)]">
{submitError && (
<AlertBanner variant="error" title="Error" className="mb-6">
{submitError}
</AlertBanner>
)}
<form onSubmit={event => void form.handleSubmit(event)} className="space-y-4">
<FormField label="Name" error={form.touched.name ? form.errors.name : undefined} required>
<Input
value={form.values.name}
onChange={e => form.setValue("name", e.target.value)}
onBlur={() => form.setTouchedField("name")}
placeholder="Your name"
/>
</FormField>
<FormField
label="Email"
error={form.touched.email ? form.errors.email : undefined}
required
>
<Input
type="email"
value={form.values.email}
onChange={e => form.setValue("email", e.target.value)}
onBlur={() => form.setTouchedField("email")}
placeholder="your@email.com"
/>
</FormField>
<FormField
label="Phone (Optional)"
error={form.touched.phone ? form.errors.phone : undefined}
>
<Input
value={form.values.phone ?? ""}
onChange={e => form.setValue("phone", e.target.value)}
onBlur={() => form.setTouchedField("phone")}
placeholder="+81 90-1234-5678"
/>
</FormField>
<FormField
label="Subject"
error={form.touched.subject ? form.errors.subject : undefined}
required
>
<Input
value={form.values.subject}
onChange={e => form.setValue("subject", e.target.value)}
onBlur={() => form.setTouchedField("subject")}
placeholder="How can we help?"
/>
</FormField>
<FormField
label="Message"
error={form.touched.message ? form.errors.message : undefined}
required
>
<textarea
className="flex min-h-[120px] w-full rounded-xl border border-input bg-background px-4 py-3 text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:border-transparent disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
value={form.values.message}
onChange={e => form.setValue("message", e.target.value)}
onBlur={() => form.setTouchedField("message")}
placeholder="Tell us more about your inquiry..."
rows={5}
/>
</FormField>
<Button
type="submit"
className="w-full"
disabled={form.isSubmitting}
isLoading={form.isSubmitting}
loadingText="Sending..."
>
Send Message
</Button>
</form>
</div>
{/* Privacy note */}
<p className="text-xs text-muted-foreground text-center mt-4">
By submitting this form, you agree to our{" "}
<Link href="#" className="text-primary hover:underline">
Privacy Policy
</Link>
.
</p>
</div>
);
}

View File

@ -46,83 +46,91 @@ const FAQ_ITEMS = [
*/
export function PublicSupportView() {
return (
<div className="max-w-4xl mx-auto">
<div className="max-w-4xl mx-auto space-y-16">
{/* Header */}
<div className="text-center mb-12">
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary/10 rounded-full mb-4">
<QuestionMarkCircleIcon className="h-8 w-8 text-primary" />
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary/10 rounded-2xl mb-2 text-primary">
<QuestionMarkCircleIcon className="h-8 w-8" />
</div>
<h1 className="text-3xl font-bold text-foreground mb-2">How can we help?</h1>
<p className="text-muted-foreground">
<h1 className="text-3xl font-bold text-foreground">How can we help?</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Find answers to common questions or get in touch with our support team.
</p>
</div>
{/* Contact Options */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Link
href="/contact"
className="bg-card rounded-xl border border-border p-6 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)] transition-all group"
className="group bg-card rounded-2xl border border-border p-6 hover:border-primary/50 hover:shadow-md transition-all duration-300"
>
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
<EnvelopeIcon className="h-6 w-6 text-primary" />
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center flex-shrink-0 text-primary">
<EnvelopeIcon className="h-6 w-6" />
</div>
<div className="flex-1">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground group-hover:text-primary transition-colors">
Send us a message
</h3>
<p className="text-sm text-muted-foreground mt-1">
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed">
Fill out our contact form and we'll get back to you within 24 hours.
</p>
</div>
<ChevronRightIcon className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors" />
<ChevronRightIcon className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors mt-1" />
</div>
</Link>
<a
href="mailto:support@assist-solutions.jp"
className="bg-card rounded-xl border border-border p-6 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)] transition-all group"
className="group bg-card rounded-2xl border border-border p-6 hover:border-primary/50 hover:shadow-md transition-all duration-300"
>
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-info/10 rounded-full flex items-center justify-center flex-shrink-0">
<ChatBubbleLeftRightIcon className="h-6 w-6 text-info" />
<div className="w-12 h-12 bg-blue-500/10 rounded-xl flex items-center justify-center flex-shrink-0 text-blue-500">
<ChatBubbleLeftRightIcon className="h-6 w-6" />
</div>
<div className="flex-1">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground group-hover:text-primary transition-colors">
Email Support
</h3>
<p className="text-sm text-muted-foreground mt-1">support@assist-solutions.jp</p>
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed truncate">
support@assist-solutions.jp
</p>
</div>
<ChevronRightIcon className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors" />
<ChevronRightIcon className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors mt-1" />
</div>
</a>
</div>
{/* FAQ Section */}
<div className="bg-card rounded-xl border border-border p-8 shadow-[var(--cp-shadow-1)]">
<h2 className="text-xl font-semibold text-foreground mb-6">Frequently Asked Questions</h2>
<div className="space-y-4">
<div className="space-y-6">
<h2 className="text-2xl font-semibold text-foreground text-center">
Frequently Asked Questions
</h2>
<div className="bg-card rounded-2xl border border-border divide-y divide-border">
{FAQ_ITEMS.map((item, index) => (
<details
key={index}
className="group border-b border-border last:border-0 pb-4 last:pb-0"
>
<summary className="flex items-center justify-between cursor-pointer list-none py-2">
<span className="font-medium text-foreground pr-4">{item.question}</span>
<details key={index} className="group">
<summary className="flex items-center justify-between cursor-pointer list-none p-6 hover:bg-muted/30 transition-colors">
<span className="font-medium text-foreground pr-4 group-hover:text-primary transition-colors">
{item.question}
</span>
<ChevronRightIcon className="h-5 w-5 text-muted-foreground group-open:rotate-90 transition-transform flex-shrink-0" />
</summary>
<p className="text-muted-foreground text-sm mt-2 pl-0">{item.answer}</p>
<div className="px-6 pb-6 pt-0">
<p className="text-muted-foreground text-sm leading-relaxed">{item.answer}</p>
</div>
</details>
))}
</div>
</div>
{/* Existing Customer */}
<div className="mt-8 text-center">
<p className="text-muted-foreground mb-4">
<div className="text-center pt-8 border-t border-border">
<p className="text-muted-foreground">
Already have an account?{" "}
<Link href="/auth/login" className="text-primary hover:underline">
<Link
href="/auth/login"
className="font-semibold text-primary hover:text-primary/80 hover:underline transition-colors"
>
Sign in
</Link>{" "}
to access your dashboard and support cases.

View File

@ -3,7 +3,7 @@
This guide describes how eligibility and verification work in the customer portal:
- **Internet eligibility** (NTT serviceability review)
- **SIM ID verification** (residence card / identity document)
- **ID verification** (residence card / identity document) for all services
## Overview
@ -13,13 +13,13 @@ This guide describes how eligibility and verification work in the customer porta
| Payment methods | WHMCS | Card storage via Stripe |
| Orders + fulfillment | Salesforce Order (and downstream WHMCS) | Operational workflow |
| Internet eligibility status | Salesforce Account (with Case for workflow) | Reuse for future internet orders |
| SIM ID verification status | Salesforce Account (with Files) | Reuse for future SIM orders |
| ID verification status | Salesforce Account (with Files) | Reuse for future orders |
## Internet Eligibility (NTT Address Review)
### How It Works
1. Customer navigates to `/account/shop/internet`
1. Customer navigates to `/account/services/internet`
2. Customer enters service address and requests eligibility check
3. Portal **finds/creates a Salesforce Opportunity** (Stage = `Introduction`) and creates a Salesforce Case **linked to that Opportunity** for agent review
4. Agent performs NTT serviceability check (manual process)
@ -62,7 +62,7 @@ const isInternetService =
| `Eligible` | Show eligible plans | Allow submit |
| `Ineligible` | Show "Not available" + contact support | Block submit |
## SIM ID Verification (Residence Card)
## ID Verification (Residence Card)
### How It Works
@ -70,7 +70,7 @@ const isInternetService =
2. Customer uploads residence card in the "Identity Verification" section
3. File is uploaded to Salesforce (ContentVersion linked to Account)
4. Agent reviews document and updates verification status
5. Customer sees "Verified" status and can order SIM
5. Customer sees "Verified" status and can submit orders
### Where to Upload
@ -93,12 +93,12 @@ The Profile page is the primary location. The standalone page is used when redir
### Status Values
| Status | Portal UI | Can Order SIM? |
| --------------- | --------------------------------------- | -------------: |
| `Not Submitted` | Show upload form | No |
| `Submitted` | Show "Under Review" with submitted info | Yes |
| `Verified` | Show "Verified" badge | Yes |
| `Rejected` | Show rejection reason + upload form | No |
| Status | Portal UI | Can Submit Order? |
| --------------- | --------------------------------------- | ----------------: |
| `Not Submitted` | Show upload form | No |
| `Submitted` | Show "Under Review" with submitted info | Yes |
| `Verified` | Show "Verified" badge | Yes |
| `Rejected` | Show rejection reason + upload form | No |
### Supported File Types
@ -108,11 +108,11 @@ The Profile page is the primary location. The standalone page is used when redir
## Portal UI Locations
| Location | What's Shown |
| ------------------------ | ----------------------------------------------- |
| `/account/settings` | Profile, Address, ID Verification (with upload) |
| `/account/shop/internet` | Eligibility status and eligible plans |
| Subscription detail | Service-specific actions (cancel, etc.) |
| Location | What's Shown |
| ---------------------------- | ----------------------------------------------- |
| `/account/settings` | Profile, Address, ID Verification (with upload) |
| `/account/services/internet` | Eligibility status and eligible plans |
| Subscription detail | Service-specific actions (cancel, etc.) |
## Cancellation Flow

View File

@ -56,7 +56,7 @@ export const NOTIFICATION_TEMPLATES: Record<NotificationTypeValue, NotificationT
title: "Good news! Internet service is available",
message:
"We've confirmed internet service is available at your address. You can now select a plan and complete your order.",
actionUrl: "/account/shop/internet",
actionUrl: "/account/services/internet",
actionLabel: "View Plans",
priority: "high",
},