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:
parent
47414f10e0
commit
3bb4e8ce40
@ -201,7 +201,7 @@ export class MeStatusService {
|
||||
description:
|
||||
"We’re verifying if our service is available at your residence. We’ll 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",
|
||||
});
|
||||
}
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import InternetCancelContainer from "@/features/subscriptions/views/InternetCancel";
|
||||
|
||||
export default function AccountInternetCancelPage() {
|
||||
return <InternetCancelContainer />;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import SubscriptionDetailContainer from "@/features/subscriptions/views/SubscriptionDetail";
|
||||
|
||||
export default function AccountServiceDetailPage() {
|
||||
return <SubscriptionDetailContainer />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import SimCallHistoryContainer from "@/features/subscriptions/views/SimCallHistory";
|
||||
|
||||
export default function AccountSimCallHistoryPage() {
|
||||
return <SimCallHistoryContainer />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import SimCancelContainer from "@/features/subscriptions/views/SimCancel";
|
||||
|
||||
export default function AccountSimCancelPage() {
|
||||
return <SimCancelContainer />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import SimChangePlanContainer from "@/features/subscriptions/views/SimChangePlan";
|
||||
|
||||
export default function AccountSimChangePlanPage() {
|
||||
return <SimChangePlanContainer />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import SimReissueContainer from "@/features/subscriptions/views/SimReissue";
|
||||
|
||||
export default function AccountSimReissuePage() {
|
||||
return <SimReissueContainer />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import SimTopUpContainer from "@/features/subscriptions/views/SimTopUp";
|
||||
|
||||
export default function AccountSimTopUpPage() {
|
||||
return <SimTopUpContainer />;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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 />;
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import { InternetConfigureContainer } from "@/features/catalog/views/InternetConfigure";
|
||||
|
||||
export default function AccountInternetConfigurePage() {
|
||||
return <InternetConfigureContainer />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import { InternetPlansContainer } from "@/features/catalog/views/InternetPlans";
|
||||
|
||||
export default function AccountInternetPlansPage() {
|
||||
return <InternetPlansContainer />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export default function AccountShopLayout({ children }: { children: ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import { CatalogHomeView } from "@/features/catalog/views/CatalogHome";
|
||||
|
||||
export default function AccountShopPage() {
|
||||
return <CatalogHomeView />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import { SimConfigureContainer } from "@/features/catalog/views/SimConfigure";
|
||||
|
||||
export default function AccountSimConfigurePage() {
|
||||
return <SimConfigureContainer />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import { SimPlansContainer } from "@/features/catalog/views/SimPlans";
|
||||
|
||||
export default function AccountSimPlansPage() {
|
||||
return <SimPlansContainer />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import { VpnPlansView } from "@/features/catalog/views/VpnPlans";
|
||||
|
||||
export default function AccountVpnPlansPage() {
|
||||
return <VpnPlansView />;
|
||||
}
|
||||
@ -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],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -5,3 +5,4 @@
|
||||
|
||||
export { AppShell } from "./AppShell/AppShell";
|
||||
export { AgentforceWidget } from "./AgentforceWidget";
|
||||
export { SiteFooter } from "./SiteFooter";
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export { CatalogNav, CatalogShell } from "./CatalogShell";
|
||||
export type { CatalogShellProps } from "./CatalogShell";
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
>
|
||||
|
||||
@ -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 tiers—difference 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'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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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'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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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)}
|
||||
|
||||
115
apps/portal/src/features/catalog/components/sim/SimFees.tsx
Normal file
115
apps/portal/src/features/catalog/components/sim/SimFees.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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" />}
|
||||
>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,3 +3,4 @@ export * from "./useConfigureParams";
|
||||
export * from "./useSimConfigure";
|
||||
export * from "./useInternetConfigure";
|
||||
export * from "./useInternetEligibility";
|
||||
export * from "./useServicesBasePath";
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@ -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";
|
||||
}
|
||||
@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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't find any internet plans available at this time.
|
||||
We couldn'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>
|
||||
)}
|
||||
|
||||
@ -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'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'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'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>
|
||||
);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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'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'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> = {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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'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"
|
||||
|
||||
@ -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">
|
||||
We’re verifying your residence card. We’ll 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'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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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'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"
|
||||
|
||||
@ -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>
|
||||
We’ll 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 couldn’t 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). We’ll 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>
|
||||
We’ll 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 couldn’t 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). We’ll 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>• You’ll receive confirmation and next steps by email</p>
|
||||
</div>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -68,7 +68,7 @@ export function CheckoutStatusBanners({
|
||||
We’re 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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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'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>
|
||||
|
||||
@ -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>
|
||||
) : (
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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'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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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",
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user