From 3bb4e8ce40e460acab8e577c672f3c6d5ac13c69 Mon Sep 17 00:00:00 2001 From: barsa Date: Wed, 24 Dec 2025 19:01:21 +0900 Subject: [PATCH] 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. --- .../modules/me-status/me-status.service.ts | 4 +- .../services/order-validator.service.ts | 2 +- .../src/app/(public)/(catalog)/layout.tsx | 11 - .../shop/internet/configure/page.tsx | 17 - .../(public)/(catalog)/shop/internet/page.tsx | 17 - .../app/(public)/(catalog)/shop/layout.tsx | 9 - .../app/(public)/(catalog)/shop/loading.tsx | 28 - .../src/app/(public)/(catalog)/shop/page.tsx | 17 - .../(catalog)/shop/sim/configure/page.tsx | 17 - .../app/(public)/(catalog)/shop/sim/page.tsx | 17 - .../app/(public)/(catalog)/shop/vpn/page.tsx | 17 - .../services/[id]/internet/cancel/page.tsx | 5 - .../src/app/account/services/[id]/loading.tsx | 19 - .../src/app/account/services/[id]/page.tsx | 5 - .../services/[id]/sim/call-history/page.tsx | 5 - .../account/services/[id]/sim/cancel/page.tsx | 5 - .../services/[id]/sim/change-plan/page.tsx | 5 - .../services/[id]/sim/reissue/page.tsx | 5 - .../account/services/[id]/sim/top-up/page.tsx | 5 - .../src/app/account/services/loading.tsx | 16 - apps/portal/src/app/account/services/page.tsx | 6 +- .../account/shop/internet/configure/page.tsx | 5 - .../src/app/account/shop/internet/page.tsx | 5 - apps/portal/src/app/account/shop/layout.tsx | 5 - apps/portal/src/app/account/shop/page.tsx | 5 - .../app/account/shop/sim/configure/page.tsx | 5 - apps/portal/src/app/account/shop/sim/page.tsx | 5 - apps/portal/src/app/account/shop/vpn/page.tsx | 5 - .../organisms/AppShell/navigation.ts | 8 +- apps/portal/src/components/organisms/index.ts | 1 + .../templates/AuthLayout/AuthLayout.tsx | 29 +- .../templates/CatalogShell/CatalogShell.tsx | 127 --- .../templates/CatalogShell/index.ts | 2 - .../templates/PublicShell/PublicShell.tsx | 85 +- apps/portal/src/components/templates/index.ts | 3 - .../components/base/ServiceHighlights.tsx | 50 + .../catalog/components/base/ShopTabs.tsx | 48 - .../RedirectAuthenticatedToAccountShop.tsx | 37 - .../src/features/catalog/components/index.ts | 5 + .../internet/InternetImportantNotes.tsx | 2 +- .../internet/InternetOfferingCard.tsx | 265 ++--- .../components/internet/InternetPlanCard.tsx | 8 +- .../internet/PlanComparisonGuide.tsx | 114 ++- .../internet/PublicOfferingCard.tsx | 16 +- .../components/internet/WhyChooseSection.tsx | 81 -- .../internet/WhyChooseUsPillars.tsx | 106 -- .../configure/InternetConfigureContainer.tsx | 6 +- .../components/sim/SimCallingRates.tsx | 168 ++++ .../components/sim/SimConfigureView.tsx | 22 +- .../catalog/components/sim/SimFees.tsx | 115 +++ .../components/sim/SimOrderProcess.tsx | 71 ++ .../catalog/components/sim/SimPlanCard.tsx | 6 +- .../components/sim/SimTypeComparison.tsx | 157 +++ .../components/sim/SimTypeSelector.tsx | 243 +++-- .../src/features/catalog/hooks/index.ts | 1 + .../catalog/hooks/useInternetConfigure.ts | 8 +- .../features/catalog/hooks/useShopBasePath.ts | 17 - .../features/catalog/hooks/useSimConfigure.ts | 8 +- .../features/catalog/views/CatalogHome.tsx | 10 +- .../features/catalog/views/InternetPlans.tsx | 683 ++++++------- .../catalog/views/PublicCatalogHome.tsx | 246 ++--- .../catalog/views/PublicInternetConfigure.tsx | 12 +- .../catalog/views/PublicInternetPlans.tsx | 123 ++- .../catalog/views/PublicSimConfigure.tsx | 26 +- .../features/catalog/views/PublicSimPlans.tsx | 692 ++++++++----- .../features/catalog/views/PublicVpnPlans.tsx | 10 +- .../src/features/catalog/views/SimPlans.tsx | 918 ++++++++++-------- .../src/features/catalog/views/VpnPlans.tsx | 10 +- .../components/AccountCheckoutContainer.tsx | 654 ++++++------- .../checkout/components/CheckoutEntry.tsx | 2 +- .../components/CheckoutErrorBoundary.tsx | 2 +- .../components/CheckoutStatusBanners.tsx | 2 +- .../checkout/components/EmptyCartRedirect.tsx | 10 +- .../dashboard/components/TaskList.tsx | 2 +- .../landing-page/views/PublicLandingView.tsx | 352 ++++--- .../src/features/orders/views/OrdersList.tsx | 2 +- .../subscriptions/views/SubscriptionsList.tsx | 2 +- .../support/views/PublicContactView.tsx | 333 ++++--- .../support/views/PublicSupportView.tsx | 72 +- .../eligibility-and-verification.md | 32 +- packages/domain/notifications/schema.ts | 2 +- 81 files changed, 3318 insertions(+), 2955 deletions(-) delete mode 100644 apps/portal/src/app/(public)/(catalog)/layout.tsx delete mode 100644 apps/portal/src/app/(public)/(catalog)/shop/internet/configure/page.tsx delete mode 100644 apps/portal/src/app/(public)/(catalog)/shop/internet/page.tsx delete mode 100644 apps/portal/src/app/(public)/(catalog)/shop/layout.tsx delete mode 100644 apps/portal/src/app/(public)/(catalog)/shop/loading.tsx delete mode 100644 apps/portal/src/app/(public)/(catalog)/shop/page.tsx delete mode 100644 apps/portal/src/app/(public)/(catalog)/shop/sim/configure/page.tsx delete mode 100644 apps/portal/src/app/(public)/(catalog)/shop/sim/page.tsx delete mode 100644 apps/portal/src/app/(public)/(catalog)/shop/vpn/page.tsx delete mode 100644 apps/portal/src/app/account/services/[id]/internet/cancel/page.tsx delete mode 100644 apps/portal/src/app/account/services/[id]/loading.tsx delete mode 100644 apps/portal/src/app/account/services/[id]/page.tsx delete mode 100644 apps/portal/src/app/account/services/[id]/sim/call-history/page.tsx delete mode 100644 apps/portal/src/app/account/services/[id]/sim/cancel/page.tsx delete mode 100644 apps/portal/src/app/account/services/[id]/sim/change-plan/page.tsx delete mode 100644 apps/portal/src/app/account/services/[id]/sim/reissue/page.tsx delete mode 100644 apps/portal/src/app/account/services/[id]/sim/top-up/page.tsx delete mode 100644 apps/portal/src/app/account/services/loading.tsx delete mode 100644 apps/portal/src/app/account/shop/internet/configure/page.tsx delete mode 100644 apps/portal/src/app/account/shop/internet/page.tsx delete mode 100644 apps/portal/src/app/account/shop/layout.tsx delete mode 100644 apps/portal/src/app/account/shop/page.tsx delete mode 100644 apps/portal/src/app/account/shop/sim/configure/page.tsx delete mode 100644 apps/portal/src/app/account/shop/sim/page.tsx delete mode 100644 apps/portal/src/app/account/shop/vpn/page.tsx delete mode 100644 apps/portal/src/components/templates/CatalogShell/CatalogShell.tsx delete mode 100644 apps/portal/src/components/templates/CatalogShell/index.ts create mode 100644 apps/portal/src/features/catalog/components/base/ServiceHighlights.tsx delete mode 100644 apps/portal/src/features/catalog/components/base/ShopTabs.tsx delete mode 100644 apps/portal/src/features/catalog/components/common/RedirectAuthenticatedToAccountShop.tsx delete mode 100644 apps/portal/src/features/catalog/components/internet/WhyChooseSection.tsx delete mode 100644 apps/portal/src/features/catalog/components/internet/WhyChooseUsPillars.tsx create mode 100644 apps/portal/src/features/catalog/components/sim/SimCallingRates.tsx create mode 100644 apps/portal/src/features/catalog/components/sim/SimFees.tsx create mode 100644 apps/portal/src/features/catalog/components/sim/SimOrderProcess.tsx create mode 100644 apps/portal/src/features/catalog/components/sim/SimTypeComparison.tsx delete mode 100644 apps/portal/src/features/catalog/hooks/useShopBasePath.ts diff --git a/apps/bff/src/modules/me-status/me-status.service.ts b/apps/bff/src/modules/me-status/me-status.service.ts index 9ece5def..d8ed3692 100644 --- a/apps/bff/src/modules/me-status/me-status.service.ts +++ b/apps/bff/src/modules/me-status/me-status.service.ts @@ -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", }); } diff --git a/apps/bff/src/modules/orders/services/order-validator.service.ts b/apps/bff/src/modules/orders/services/order-validator.service.ts index 1b0c0255..9f24b5fb 100644 --- a/apps/bff/src/modules/orders/services/order-validator.service.ts +++ b/apps/bff/src/modules/orders/services/order-validator.service.ts @@ -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") { diff --git a/apps/portal/src/app/(public)/(catalog)/layout.tsx b/apps/portal/src/app/(public)/(catalog)/layout.tsx deleted file mode 100644 index c40e3123..00000000 --- a/apps/portal/src/app/(public)/(catalog)/layout.tsx +++ /dev/null @@ -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 {children}; -} diff --git a/apps/portal/src/app/(public)/(catalog)/shop/internet/configure/page.tsx b/apps/portal/src/app/(public)/(catalog)/shop/internet/configure/page.tsx deleted file mode 100644 index 9c763561..00000000 --- a/apps/portal/src/app/(public)/(catalog)/shop/internet/configure/page.tsx +++ /dev/null @@ -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 ( - <> - - - - ); -} diff --git a/apps/portal/src/app/(public)/(catalog)/shop/internet/page.tsx b/apps/portal/src/app/(public)/(catalog)/shop/internet/page.tsx deleted file mode 100644 index 2b2c557d..00000000 --- a/apps/portal/src/app/(public)/(catalog)/shop/internet/page.tsx +++ /dev/null @@ -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 ( - <> - - - - ); -} diff --git a/apps/portal/src/app/(public)/(catalog)/shop/layout.tsx b/apps/portal/src/app/(public)/(catalog)/shop/layout.tsx deleted file mode 100644 index 93dcb7e4..00000000 --- a/apps/portal/src/app/(public)/(catalog)/shop/layout.tsx +++ /dev/null @@ -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; -} diff --git a/apps/portal/src/app/(public)/(catalog)/shop/loading.tsx b/apps/portal/src/app/(public)/(catalog)/shop/loading.tsx deleted file mode 100644 index 06293c5c..00000000 --- a/apps/portal/src/app/(public)/(catalog)/shop/loading.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Skeleton } from "@/components/atoms/loading-skeleton"; - -export default function CatalogLoading() { - return ( -
-
- - - -
- -
- {Array.from({ length: 3 }).map((_, i) => ( -
- - - - - -
- ))} -
-
- ); -} diff --git a/apps/portal/src/app/(public)/(catalog)/shop/page.tsx b/apps/portal/src/app/(public)/(catalog)/shop/page.tsx deleted file mode 100644 index e1fc3a22..00000000 --- a/apps/portal/src/app/(public)/(catalog)/shop/page.tsx +++ /dev/null @@ -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 ( - <> - - - - ); -} diff --git a/apps/portal/src/app/(public)/(catalog)/shop/sim/configure/page.tsx b/apps/portal/src/app/(public)/(catalog)/shop/sim/configure/page.tsx deleted file mode 100644 index 3688ba08..00000000 --- a/apps/portal/src/app/(public)/(catalog)/shop/sim/configure/page.tsx +++ /dev/null @@ -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 ( - <> - - - - ); -} diff --git a/apps/portal/src/app/(public)/(catalog)/shop/sim/page.tsx b/apps/portal/src/app/(public)/(catalog)/shop/sim/page.tsx deleted file mode 100644 index 6ba056eb..00000000 --- a/apps/portal/src/app/(public)/(catalog)/shop/sim/page.tsx +++ /dev/null @@ -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 ( - <> - - - - ); -} diff --git a/apps/portal/src/app/(public)/(catalog)/shop/vpn/page.tsx b/apps/portal/src/app/(public)/(catalog)/shop/vpn/page.tsx deleted file mode 100644 index 47d3c37d..00000000 --- a/apps/portal/src/app/(public)/(catalog)/shop/vpn/page.tsx +++ /dev/null @@ -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 ( - <> - - - - ); -} diff --git a/apps/portal/src/app/account/services/[id]/internet/cancel/page.tsx b/apps/portal/src/app/account/services/[id]/internet/cancel/page.tsx deleted file mode 100644 index e1684c84..00000000 --- a/apps/portal/src/app/account/services/[id]/internet/cancel/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import InternetCancelContainer from "@/features/subscriptions/views/InternetCancel"; - -export default function AccountInternetCancelPage() { - return ; -} diff --git a/apps/portal/src/app/account/services/[id]/loading.tsx b/apps/portal/src/app/account/services/[id]/loading.tsx deleted file mode 100644 index 70d2b15f..00000000 --- a/apps/portal/src/app/account/services/[id]/loading.tsx +++ /dev/null @@ -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 ( - } - title="Service" - description="Service details" - mode="content" - > -
- - -
-
- ); -} diff --git a/apps/portal/src/app/account/services/[id]/page.tsx b/apps/portal/src/app/account/services/[id]/page.tsx deleted file mode 100644 index 2324febc..00000000 --- a/apps/portal/src/app/account/services/[id]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import SubscriptionDetailContainer from "@/features/subscriptions/views/SubscriptionDetail"; - -export default function AccountServiceDetailPage() { - return ; -} diff --git a/apps/portal/src/app/account/services/[id]/sim/call-history/page.tsx b/apps/portal/src/app/account/services/[id]/sim/call-history/page.tsx deleted file mode 100644 index 16e52603..00000000 --- a/apps/portal/src/app/account/services/[id]/sim/call-history/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import SimCallHistoryContainer from "@/features/subscriptions/views/SimCallHistory"; - -export default function AccountSimCallHistoryPage() { - return ; -} diff --git a/apps/portal/src/app/account/services/[id]/sim/cancel/page.tsx b/apps/portal/src/app/account/services/[id]/sim/cancel/page.tsx deleted file mode 100644 index f9aaf9a4..00000000 --- a/apps/portal/src/app/account/services/[id]/sim/cancel/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import SimCancelContainer from "@/features/subscriptions/views/SimCancel"; - -export default function AccountSimCancelPage() { - return ; -} diff --git a/apps/portal/src/app/account/services/[id]/sim/change-plan/page.tsx b/apps/portal/src/app/account/services/[id]/sim/change-plan/page.tsx deleted file mode 100644 index 8ff1da30..00000000 --- a/apps/portal/src/app/account/services/[id]/sim/change-plan/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import SimChangePlanContainer from "@/features/subscriptions/views/SimChangePlan"; - -export default function AccountSimChangePlanPage() { - return ; -} diff --git a/apps/portal/src/app/account/services/[id]/sim/reissue/page.tsx b/apps/portal/src/app/account/services/[id]/sim/reissue/page.tsx deleted file mode 100644 index 1936a048..00000000 --- a/apps/portal/src/app/account/services/[id]/sim/reissue/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import SimReissueContainer from "@/features/subscriptions/views/SimReissue"; - -export default function AccountSimReissuePage() { - return ; -} diff --git a/apps/portal/src/app/account/services/[id]/sim/top-up/page.tsx b/apps/portal/src/app/account/services/[id]/sim/top-up/page.tsx deleted file mode 100644 index 89629c2e..00000000 --- a/apps/portal/src/app/account/services/[id]/sim/top-up/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import SimTopUpContainer from "@/features/subscriptions/views/SimTopUp"; - -export default function AccountSimTopUpPage() { - return ; -} diff --git a/apps/portal/src/app/account/services/loading.tsx b/apps/portal/src/app/account/services/loading.tsx deleted file mode 100644 index 3db64007..00000000 --- a/apps/portal/src/app/account/services/loading.tsx +++ /dev/null @@ -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 ( - } - title="Services" - description="View and manage your services" - mode="content" - > - - - ); -} diff --git a/apps/portal/src/app/account/services/page.tsx b/apps/portal/src/app/account/services/page.tsx index 13e6e5bb..b4b2c55c 100644 --- a/apps/portal/src/app/account/services/page.tsx +++ b/apps/portal/src/app/account/services/page.tsx @@ -1,5 +1,5 @@ -import SubscriptionsListContainer from "@/features/subscriptions/views/SubscriptionsList"; +import { CatalogHomeView } from "@/features/catalog/views/CatalogHome"; -export default function AccountServicesPage() { - return ; +export default function AccountShopPage() { + return ; } diff --git a/apps/portal/src/app/account/shop/internet/configure/page.tsx b/apps/portal/src/app/account/shop/internet/configure/page.tsx deleted file mode 100644 index f4c70ebb..00000000 --- a/apps/portal/src/app/account/shop/internet/configure/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { InternetConfigureContainer } from "@/features/catalog/views/InternetConfigure"; - -export default function AccountInternetConfigurePage() { - return ; -} diff --git a/apps/portal/src/app/account/shop/internet/page.tsx b/apps/portal/src/app/account/shop/internet/page.tsx deleted file mode 100644 index 469f6ade..00000000 --- a/apps/portal/src/app/account/shop/internet/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { InternetPlansContainer } from "@/features/catalog/views/InternetPlans"; - -export default function AccountInternetPlansPage() { - return ; -} diff --git a/apps/portal/src/app/account/shop/layout.tsx b/apps/portal/src/app/account/shop/layout.tsx deleted file mode 100644 index f0737f4d..00000000 --- a/apps/portal/src/app/account/shop/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import type { ReactNode } from "react"; - -export default function AccountShopLayout({ children }: { children: ReactNode }) { - return <>{children}; -} diff --git a/apps/portal/src/app/account/shop/page.tsx b/apps/portal/src/app/account/shop/page.tsx deleted file mode 100644 index b4b2c55c..00000000 --- a/apps/portal/src/app/account/shop/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { CatalogHomeView } from "@/features/catalog/views/CatalogHome"; - -export default function AccountShopPage() { - return ; -} diff --git a/apps/portal/src/app/account/shop/sim/configure/page.tsx b/apps/portal/src/app/account/shop/sim/configure/page.tsx deleted file mode 100644 index ab66d78c..00000000 --- a/apps/portal/src/app/account/shop/sim/configure/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { SimConfigureContainer } from "@/features/catalog/views/SimConfigure"; - -export default function AccountSimConfigurePage() { - return ; -} diff --git a/apps/portal/src/app/account/shop/sim/page.tsx b/apps/portal/src/app/account/shop/sim/page.tsx deleted file mode 100644 index e00978cf..00000000 --- a/apps/portal/src/app/account/shop/sim/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { SimPlansContainer } from "@/features/catalog/views/SimPlans"; - -export default function AccountSimPlansPage() { - return ; -} diff --git a/apps/portal/src/app/account/shop/vpn/page.tsx b/apps/portal/src/app/account/shop/vpn/page.tsx deleted file mode 100644 index 701c37c7..00000000 --- a/apps/portal/src/app/account/shop/vpn/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { VpnPlansView } from "@/features/catalog/views/VpnPlans"; - -export default function AccountVpnPlansPage() { - return ; -} diff --git a/apps/portal/src/components/organisms/AppShell/navigation.ts b/apps/portal/src/components/organisms/AppShell/navigation.ts index 1a39796c..ba64f773 100644 --- a/apps/portal/src/components/organisms/AppShell/navigation.ts +++ b/apps/portal/src/components/organisms/AppShell/navigation.ts @@ -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], }; } diff --git a/apps/portal/src/components/organisms/index.ts b/apps/portal/src/components/organisms/index.ts index d89ae25d..86db9862 100644 --- a/apps/portal/src/components/organisms/index.ts +++ b/apps/portal/src/components/organisms/index.ts @@ -5,3 +5,4 @@ export { AppShell } from "./AppShell/AppShell"; export { AgentforceWidget } from "./AgentforceWidget"; +export { SiteFooter } from "./SiteFooter"; diff --git a/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx b/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx index b045cbfa..8e9b020d 100644 --- a/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx +++ b/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx @@ -42,19 +42,14 @@ export function AuthLayout({ )}
-
-
-
-
- -
+
+
+
-

- {title} -

+

{title}

{subtitle && ( -

+

{subtitle}

)} @@ -62,18 +57,14 @@ export function AuthLayout({
-
- {/* Subtle gradient glow behind card */} -
- -
- {children} -
+
+ {children}
{/* Trust indicator */} -
-

+

+

+ Secure login protected by SSL encryption

diff --git a/apps/portal/src/components/templates/CatalogShell/CatalogShell.tsx b/apps/portal/src/components/templates/CatalogShell/CatalogShell.tsx deleted file mode 100644 index 3cbce476..00000000 --- a/apps/portal/src/components/templates/CatalogShell/CatalogShell.tsx +++ /dev/null @@ -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 ; -} - -export function CatalogShell({ children }: CatalogShellProps) { - const isAuthenticated = useAuthStore(state => state.isAuthenticated); - const hasCheckedAuth = useAuthStore(state => state.hasCheckedAuth); - const checkAuth = useAuthStore(state => state.checkAuth); - - useEffect(() => { - if (!hasCheckedAuth) { - void checkAuth(); - } - }, [checkAuth, hasCheckedAuth]); - - return ( -
- {/* Subtle background pattern */} -
-
-
-
- -
-
- {/* Logo */} - - - - - - - Assist Solutions - - - Account Portal - - - - - {/* Right side actions */} -
- - Support - - {isAuthenticated ? ( - - My Account - - ) : ( - - Sign in - - )} -
-
-
- - - -
-
- {children} -
-
- -
-
-
-
- © {new Date().getFullYear()} Assist Solutions. All rights reserved. -
-
- - Support - - - Privacy - - - Terms - -
-
-
-
-
- ); -} diff --git a/apps/portal/src/components/templates/CatalogShell/index.ts b/apps/portal/src/components/templates/CatalogShell/index.ts deleted file mode 100644 index 4abbb86e..00000000 --- a/apps/portal/src/components/templates/CatalogShell/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { CatalogNav, CatalogShell } from "./CatalogShell"; -export type { CatalogShellProps } from "./CatalogShell"; diff --git a/apps/portal/src/components/templates/PublicShell/PublicShell.tsx b/apps/portal/src/components/templates/PublicShell/PublicShell.tsx index a85bd1a1..54fc4a2c 100644 --- a/apps/portal/src/components/templates/PublicShell/PublicShell.tsx +++ b/apps/portal/src/components/templates/PublicShell/PublicShell.tsx @@ -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 (
- {/* Subtle background pattern */} -
-
-
-
+ {/* Subtle background pattern - clean and minimal */} +
-
-
- - - +
+
+ + + - - + + Assist Solutions - - Account Portal - -
-
-
-
-
- © {new Date().getFullYear()} Assist Solutions. All rights reserved. -
-
- - Services - - - Support - - - Privacy - - - Terms - -
-
-
-
+
); } diff --git a/apps/portal/src/components/templates/index.ts b/apps/portal/src/components/templates/index.ts index e5250a6c..42a1c1b1 100644 --- a/apps/portal/src/components/templates/index.ts +++ b/apps/portal/src/components/templates/index.ts @@ -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"; diff --git a/apps/portal/src/features/catalog/components/base/ServiceHighlights.tsx b/apps/portal/src/features/catalog/components/base/ServiceHighlights.tsx new file mode 100644 index 00000000..cf8edfb0 --- /dev/null +++ b/apps/portal/src/features/catalog/components/base/ServiceHighlights.tsx @@ -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 ( +
+
+
+ {icon} +
+ {highlight && ( + + + {highlight} + + )} +
+ +

{title}

+

{description}

+
+ ); +} + +/** + * 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 ( +
+ {features.map((feature, index) => ( + + ))} +
+ ); +} diff --git a/apps/portal/src/features/catalog/components/base/ShopTabs.tsx b/apps/portal/src/features/catalog/components/base/ShopTabs.tsx deleted file mode 100644 index ee806812..00000000 --- a/apps/portal/src/features/catalog/components/base/ShopTabs.tsx +++ /dev/null @@ -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 ( -
-
- -
-
- ); -} diff --git a/apps/portal/src/features/catalog/components/common/RedirectAuthenticatedToAccountShop.tsx b/apps/portal/src/features/catalog/components/common/RedirectAuthenticatedToAccountShop.tsx deleted file mode 100644 index 80308644..00000000 --- a/apps/portal/src/features/catalog/components/common/RedirectAuthenticatedToAccountShop.tsx +++ /dev/null @@ -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; -} diff --git a/apps/portal/src/features/catalog/components/index.ts b/apps/portal/src/features/catalog/components/index.ts index cc5786d7..4cbf1b74 100644 --- a/apps/portal/src/features/catalog/components/index.ts +++ b/apps/portal/src/features/catalog/components/index.ts @@ -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"; diff --git a/apps/portal/src/features/catalog/components/internet/InternetImportantNotes.tsx b/apps/portal/src/features/catalog/components/internet/InternetImportantNotes.tsx index 7c224b03..f773d186 100644 --- a/apps/portal/src/features/catalog/components/internet/InternetImportantNotes.tsx +++ b/apps/portal/src/features/catalog/components/internet/InternetImportantNotes.tsx @@ -8,7 +8,7 @@ export function InternetImportantNotes() {
-

Before you choose a plan

+

Important notes & fees

A few things to keep in mind when selecting your internet service.

diff --git a/apps/portal/src/features/catalog/components/internet/InternetOfferingCard.tsx b/apps/portal/src/features/catalog/components/internet/InternetOfferingCard.tsx index c2983fbf..7b5e770a 100644 --- a/apps/portal/src/features/catalog/components/internet/InternetOfferingCard.tsx +++ b/apps/portal/src/features/catalog/components/internet/InternetOfferingCard.tsx @@ -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 (
{/* Header - Always visible */} - - - {/* Expanded content - Tier options */} - {isExpanded && ( -
-
- {tiers.map(tier => ( -
+
+ {tiers.map(tier => ( +
+ {/* Header */} +
+ + {tier.tier} + + {tier.recommended && ( + )} - > - {/* Header */} -
- {tier.tier} - {tier.recommended ? ( - - ) : null} +
+ + {/* Price */} + {!previewMode && ( +
+
+ + ¥{tier.monthlyPrice.toLocaleString()} + + /mo + {tier.pricingNote && ( + {tier.pricingNote} + )} +
+ )} - {/* Pricing (hidden in preview mode) */} - {!previewMode ? ( -
-
- - ¥{tier.monthlyPrice.toLocaleString()} - - /mo -
- {tier.pricingNote ? ( -

{tier.pricingNote}

- ) : null} -
- ) : null} + {/* Description */} +

{tier.description}

- {/* Description */} -

{tier.description}

+ {/* Features */} +
    + {tier.features.slice(0, 3).map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
- {/* Features */} -
    - {(previewMode ? tier.features.slice(0, 3) : tier.features).map( - (feature, index) => ( -
  • - - - {feature} - -
  • - ) - )} -
- - {/* Button/Info - always at bottom */} - {previewMode ? ( -
-

- Prices shown after you click “See pricing” -

-
- ) : disabled ? ( -
- - {disabledReason ? ( -

- {disabledReason} -

- ) : null} -
- ) : ( - - )} -
- ))} -
- - {previewMode ? ( -
-

- Setup is typically ¥{setupFee.toLocaleString()}. Your actual options are confirmed - after address verification. -

-
-
+ ) : ( + - -
+ )}
- ) : ( -

- + ¥{setupFee.toLocaleString()} one-time installation (or 12/24-month installment) -

- )} + ))}
- )} - {/* Pricing modal (public preview mode only) */} - {previewMode ? ( - setPricingOpen(false)} - offeringTitle={title} - offeringSubtitle={`${speedBadge}${isPremium ? " · select areas" : ""}`} - tiers={tiers} - setupFee={setupFee} - ctaHref={ctaPath} - /> - ) : null} + {/* Footer */} +

+ + ¥{setupFee.toLocaleString()} one-time installation (or 12/24-month installment) +

+
); } diff --git a/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx b/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx index 6cfb4893..615fe0fc 100644 --- a/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx +++ b/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx @@ -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); }} > diff --git a/apps/portal/src/features/catalog/components/internet/PlanComparisonGuide.tsx b/apps/portal/src/features/catalog/components/internet/PlanComparisonGuide.tsx index 50b07478..4343ef67 100644 --- a/apps/portal/src/features/catalog/components/internet/PlanComparisonGuide.tsx +++ b/apps/portal/src/features/catalog/components/internet/PlanComparisonGuide.tsx @@ -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" )} > -
+
-
-

Which plan is right for you?

-

- All plans include the same connection speed. The difference is in equipment and support. -

-
+
+ {/* Collapsible header */} + - {/* Stacked rows - always vertical for cleaner reading */} -
- } - 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 && ( +
+
+ } + 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." + /> - } - 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 - /> + } + 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 + /> - } - 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." - /> -
+ } + 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." + /> +
- {/* Platinum additional info */} -
-

- About Platinum plans: 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. -

-
+
+

+ About Platinum: After verifying + your address, we'll assess your space and create a tailored proposal. Final pricing + depends on your specific setup requirements. +

+
+
+ )}
); } diff --git a/apps/portal/src/features/catalog/components/internet/PublicOfferingCard.tsx b/apps/portal/src/features/catalog/components/internet/PublicOfferingCard.tsx index e67a0dda..65773a9d 100644 --- a/apps/portal/src/features/catalog/components/internet/PublicOfferingCard.tsx +++ b/apps/portal/src/features/catalog/components/internet/PublicOfferingCard.tsx @@ -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({ {" "} (or 12/24-month installment)

- + {onCtaClick ? ( + + ) : ( + + )}
)} diff --git a/apps/portal/src/features/catalog/components/internet/WhyChooseSection.tsx b/apps/portal/src/features/catalog/components/internet/WhyChooseSection.tsx deleted file mode 100644 index f3fc9c2a..00000000 --- a/apps/portal/src/features/catalog/components/internet/WhyChooseSection.tsx +++ /dev/null @@ -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 ( -
-
- {icon} -
-
-

{title}

-

{description}

-
-
- ); -} - -export function WhyChooseSection() { - return ( -
-
-

Why choose our internet service?

-

- Japan's most reliable fiber network with dedicated English support. -

-
- -
- } - title="NTT Fiber Network" - description="Powered by Japan's largest and most reliable optical fiber infrastructure, delivering speeds up to 10Gbps." - /> - - } - title="IPoE Connection" - description="Modern IPv6/IPoE technology for congestion-free access, even during peak hours. PPPoE also available." - /> - - } - title="Flexible ISP Options" - description="Multiple connection protocols within a single contract. Switch between IPoE and PPPoE as needed." - /> - - } - title="One-Stop Solution" - description="NTT line, ISP service, and optional equipment—all managed through one provider. One bill, one contact point." - /> - - } - title="Full English Support" - description="Native English customer service for setup, billing questions, and technical support. No language barriers." - /> - - } - title="On-Site Assistance" - description="Need help at home? Our technicians can visit for setup, troubleshooting, or network optimization." - /> -
-
- ); -} diff --git a/apps/portal/src/features/catalog/components/internet/WhyChooseUsPillars.tsx b/apps/portal/src/features/catalog/components/internet/WhyChooseUsPillars.tsx deleted file mode 100644 index f3ac3aa6..00000000 --- a/apps/portal/src/features/catalog/components/internet/WhyChooseUsPillars.tsx +++ /dev/null @@ -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 ( -
-
- {icon} -
-
-

{title}

-

{description}

- {highlight && ( - - - {highlight} - - )} -
-
- ); -} - -/** - * 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: , - title: "NTT Optical Fiber", - description: "Japan's most reliable network with speeds up to 10Gbps", - highlight: "99.9% uptime", - }, - { - icon: , - title: "IPv6/IPoE Ready", - description: "Next-gen protocol for congestion-free browsing", - highlight: "No peak-hour slowdowns", - }, - { - icon: , - title: "Full English Support", - description: "Native English service for setup, billing & technical help", - highlight: "No language barriers", - }, - { - icon: , - title: "One Bill, One Provider", - description: "NTT line + ISP + equipment bundled with simple billing", - highlight: "No hidden fees", - }, - { - icon: , - title: "On-site Support", - description: "Technicians can visit for installation & troubleshooting", - highlight: "Professional setup", - }, - { - icon: , - title: "Flexible Options", - description: "Multiple ISP configs available, IPv4/PPPoE if needed", - highlight: "Customizable", - }, - ]; - - return ( -
- {/* Header */} -
-

- Why Choose Us -

-
- - {/* Feature grid - 3 columns on large, 2 on medium, 1 on mobile */} -
- {features.map((feature, index) => ( -
- -
- ))} -
-
- ); -} diff --git a/apps/portal/src/features/catalog/components/internet/configure/InternetConfigureContainer.tsx b/apps/portal/src/features/catalog/components/internet/configure/InternetConfigureContainer.tsx index 9de60fc9..25dd8906 100644 --- a/apps/portal/src/features/catalog/components/internet/configure/InternetConfigureContainer.tsx +++ b/apps/portal/src/features/catalog/components/internet/configure/InternetConfigureContainer.tsx @@ -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 */} diff --git a/apps/portal/src/features/catalog/components/sim/SimCallingRates.tsx b/apps/portal/src/features/catalog/components/sim/SimCallingRates.tsx new file mode 100644 index 00000000..9c7625da --- /dev/null +++ b/apps/portal/src/features/catalog/components/sim/SimCallingRates.tsx @@ -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 ( +
+ {/* Header */} +
+

+ + Calling & SMS Rates +

+

+ Pay-per-use charges apply. Billed 5-6 weeks after usage. +

+
+ + {/* Domestic Rates */} +
+

+ + + + Domestic (Japan) +

+ +
+
+
+ + Voice Calls +
+
+ ¥{domesticRates.calling.rate} + + /{domesticRates.calling.unit} + +
+
+ +
+
+ + SMS +
+
+ ¥{domesticRates.sms.rate} + + /{domesticRates.sms.unit} + +
+
+
+ +

Incoming calls and SMS are free.

+
+ + {/* International Rates (Collapsible) */} +
+ + + {isExpanded && ( +
+
+ + + + + + + + + {internationalCallingRates.map((rate, index) => ( + + + + + ))} + +
Country + Rate (¥/30sec) +
+ {rate.country} + ({rate.code}) + ¥{rate.rate}
+
+ +
+

• International SMS: ¥{internationalSmsRate}/message

+

• Rates vary by time of day and day of week

+

+ • For full rate details, visit{" "} + + NTT Docomo's website + +

+
+
+ )} +
+ + {/* Unlimited Calling Option */} +
+
+
+ +
+
+

Unlimited Domestic Calling

+

+ Add unlimited domestic calls to any Data+Voice plan for{" "} + ¥3,000/month +

+

+ Available as an add-on during checkout. International calls not included. +

+
+
+
+
+ ); +} diff --git a/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx b/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx index 499672d6..5488c46a 100644 --- a/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx +++ b/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx @@ -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({

Plan Not Found

The selected plan could not be found

← Return to SIM Plans @@ -190,7 +190,7 @@ export function SimConfigureView({ icon={} >
- +
@@ -535,10 +535,24 @@ export function SimConfigureView({
)} +

+ Prices exclude 10% consumption tax +

+ {/* Verification notice */} +
+

+ Next steps after checkout:{" "} + + We'll review your order and ID verification within 1-2 business days. You'll + receive an email once approved. + +

+
+
+ )} + + + ))} + + +
+
+ + + Delivery: Email after approval + +
+
+
+ + {/* Physical SIM Card */} +
+
+
+ +
+
+

Physical SIM

+

Traditional SIM card

+
+
+ +
    + {physicalSimFeatures.map((feature, index) => ( +
  • + + {feature.text} +
  • + ))} +
+ +
+
+ + + Delivery: 1-3 business days + +
+
+
+
+ + {/* EID Info Panel */} + {showEidInfo && ( +
+
+ +
+

What is an EID?

+

+ 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". +

+
+

Compatible EID prefixes:

+
+ {compatibleEidPrefixes.map(prefix => ( + + {prefix}... + + ))} +
+
+
+
+
+ )} + + {/* Note */} +

+ Both options require ID verification before activation (1-2 business days) +

+
+ ); +} diff --git a/apps/portal/src/features/catalog/components/sim/SimTypeSelector.tsx b/apps/portal/src/features/catalog/components/sim/SimTypeSelector.tsx index dfdc3c05..def836c0 100644 --- a/apps/portal/src/features/catalog/components/sim/SimTypeSelector.tsx +++ b/apps/portal/src/features/catalog/components/sim/SimTypeSelector.tsx @@ -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; } +const compatibleEidPrefixes = ["89049032", "89033023", "89033024", "89043051", "89043052"]; + export function SimTypeSelector({ simType, onSimTypeChange, @@ -15,88 +27,203 @@ export function SimTypeSelector({ onEidChange, errors, }: SimTypeSelectorProps) { - return ( -
-
- + const [showEidInfo, setShowEidInfo] = useState(false); -
diff --git a/apps/portal/src/features/catalog/views/InternetPlans.tsx b/apps/portal/src/features/catalog/views/InternetPlans.tsx index af5dffef..3e645029 100644 --- a/apps/portal/src/features/catalog/views/InternetPlans.tsx +++ b/apps/portal/src/features/catalog/views/InternetPlans.tsx @@ -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> = { "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 ( +
+ + {config.label} + {status === "eligible" && speed && ( + <> + · + Up to {speed} + + )} +
+ ); +} + 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 (
- - +
-
- {Array.from({ length: 2 }).map((_, i) => ( + {[1, 2].map(i => (
+
+ {/* Already has internet warning */} + {hasActiveInternet && ( + + You already have an internet subscription. For additional residences, please{" "} + + contact support + + . + + )} + + {/* Auto-request status alerts - only show for errors/success */} + {autoRequestStatus === "failed" && ( + + Please try again below or contact support. + + )} + + {autoRequestStatus === "missing_address" && ( + +
+ Add your service address to request availability verification. + +
+
+ )} + + +
+ + ); + } + + // Case 2: Standard Portal View (Pending, Eligible, Ineligible, Loading) return ( } >
- + - - {eligibilityLoading ? ( -
-
- Checking availability… -
-

- We're verifying what service is available at your residence. -

-
- ) : autoRequestStatus === "submitting" ? ( -
-
- - Submitting availability request - -
-

- We're sending your request now. -

-
- ) : autoRequestStatus === "submitted" ? ( -
-
- Availability review in progress -
-

- We've received your request and will notify you when the review is complete. -

-
- ) : isNotRequested ? ( -
-
- Availability review required -
-

- Request an eligibility review to unlock personalized internet plans for your - residence. -

-
- ) : isPending ? ( -
-
- Availability review in progress -
-

- We're reviewing service availability for your address. Once confirmed, we'll unlock - your personalized internet plans. -

-
- ) : isIneligible ? ( -
-
- Not available for this address -
-

- Our team reviewed your address and determined service isn't available right now. -

-
- ) : null} -
+ {/* Hero section - compact (for portal view) */} +
+

+ Your Internet Options +

+

+ Plans tailored to your residence and available infrastructure +

- {/* Auto-request status alerts */} - {autoRequestStatus === "submitting" && ( - - We're sending your request now. You'll see updated eligibility once the review begins. + {/* Status badge */} + {!eligibilityLoading && autoRequestStatus !== "submitting" && ( + + )} + + {/* Loading states */} + {(eligibilityLoading || autoRequestStatus === "submitting") && ( +
+
+ + {autoRequestStatus === "submitting" + ? "Submitting request..." + : "Checking status..."} + +
+ )} +
+ + {/* Already has internet warning */} + {hasActiveInternet && ( + + You already have an internet subscription. For additional residences, please{" "} + + contact support + + . )} + {/* Auto-request status alerts - only show for errors/success */} {autoRequestStatus === "submitted" && ( - - We've received your availability request. Our team will investigate and notify you when - the review is complete. + + We'll verify your address and notify you when complete. {autoRequestId && ( -
- Request ID: {autoRequestId} -
+ ID: {autoRequestId} )}
)} {autoRequestStatus === "failed" && ( - - Please try again below or contact support if this keeps happening. + + Please try again below or contact support. )} {autoRequestStatus === "missing_address" && ( - - Add your service address so we can submit the availability request. - - )} - - {/* Eligibility request section */} - {isNotRequested && - autoRequestStatus !== "submitting" && - autoRequestStatus !== "submitted" && ( - -
-

- Our team will verify NTT serviceability and update your eligible offerings. We'll - notify you when review is complete. -

- {hasServiceAddress ? ( - - ) : ( - - )} -
-
- )} - - {isPending && ( - -
-

- We'll notify you when review is complete. -

- {requestedAt ? ( -

- Requested: {new Date(requestedAt).toLocaleString()} -

- ) : null} -
-
- )} - - {isIneligible && ( - -
- {rejectionNotes ? ( -

{rejectionNotes}

- ) : ( -

- If you believe this is incorrect, contact support and we'll take another look. -

- )} -
)} - {hasActiveInternet && ( - -

- You already have an Internet subscription with us. If you want another subscription - for a different residence, please{" "} - - contact us - - . -

-
- )} - - {/* ELIGIBLE - Show personalized plans */} + {/* ELIGIBLE STATE - Clean & Personalized */} {isEligible && eligibilityDisplay && offeringCards.length > 0 && ( <> - {/* Eligibility confirmation box */} -
-
-
- {eligibilityDisplay.residenceType === "home" ? ( - - ) : ( - - )} -
-
-
- -

Service Available

-
-

- {eligibilityDisplay.label} -

-

{eligibilityDisplay.description}

-
-
- - - Up to {eligibilityDisplay.speed} - -
-
-
- {/* Plan comparison guide */} -
+
- {/* Available speed options */} + {/* Speed options header (only if multiple) */} {offeringCards.length > 1 && ( -
-

Choose your speed

+
+

Choose your speed

- Your address supports multiple speed options. Pick the one that fits your needs - and budget. + Your address supports multiple options

)} {/* Offering cards */} -
- {offeringCards.map((card, index) => ( +
+ {offeringCards.map(card => (
{card.isAlternative && (
- + Alternative option
@@ -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 } />
))}
- {/* Important notes */} + {/* Important notes - collapsed by default */} )} - {/* NOT ELIGIBLE YET - Show locked state */} - {orderingLocked && !isEligible && plans.length > 0 && ( + {/* PENDING STATE - Clean Status View */} + {isPending && ( <> - -

- {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."} +

+ +

+ Verification in Progress +

+

+ We're currently verifying NTT service availability at your registered address. +
+ This manual check ensures we offer you the correct fiber connection type.

- - {/* Show plan comparison guide even when locked */} -
- -
- -
-

- Preview of available plans (ordering locked) -

- {/* Show a simplified preview */} -
- {Object.entries(OFFERING_CONFIGS) - .slice(0, 4) - .map(([key, config]) => ( -
-
-
- {config.iconType === "home" ? ( - - ) : ( - - )} -
-
-

{config.title}

-

{config.speedBadge}

-
-
-
- ))} +
+ Estimated time + 1-2 business days
+ + {requestedAt && ( +

+ Request submitted: {new Date(requestedAt).toLocaleDateString()} +

+ )}
- +
+ +
)} + {/* INELIGIBLE STATE */} + {isIneligible && ( +
+ +

Service not available

+

+ {rejectionNotes || + "Our review determined that NTT fiber service isn't available at your address."} +

+ +
+ )} + {/* No plans available */} {plans.length === 0 && !isLoading && (
@@ -870,14 +752,9 @@ export function InternetPlansContainer() {

No Plans Available

- We couldn't find any internet plans available at this time. + We couldn't find any internet plans at this time.

- +
)} diff --git a/apps/portal/src/features/catalog/views/PublicCatalogHome.tsx b/apps/portal/src/features/catalog/views/PublicCatalogHome.tsx index 9b3a751a..b8071a17 100644 --- a/apps/portal/src/features/catalog/views/PublicCatalogHome.tsx +++ b/apps/portal/src/features/catalog/views/PublicCatalogHome.tsx @@ -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 ( -
-
-
- - Services Catalog -
-

- Choose your connectivity solution +
+ {/* Hero Section - Simple and focused */} +
+

+ Browse Our Services

-

- Explore our internet, mobile, and VPN services. Browse plans and pricing, then create an - account when you're ready to order. +

+ High-speed internet, mobile plans, and VPN services with full English support.

- {/* Service-specific ordering info */} -
-

What to expect when ordering

-
-
-
- + {/* Service Cards */} +
+ {/* Internet Card */} + +
+
+
+
-
-
Internet
-
- Requires address verification (1-2 business days). We'll email you when plans - are ready. -
+

+ Internet +

+

+ NTT fiber with speeds up to 10Gbps and professional installation support. +

+
    + {[ + "Up to 10Gbps speeds", + "Fiber optic network", + "WiFi router options", + "English support", + ].map((feature, i) => ( +
  • + + {feature} +
  • + ))} +
+
+ View Plans +
-
-
- + + + {/* SIM Card */} + +
+
+ + First Month Free + +
+
+
+
-
-
SIM & eSIM
-
- Order immediately after signup. Physical SIM ships next business day. -
+

+ SIM & eSIM +

+

+ Data, voice & SMS on NTT Docomo's nationwide network. +

+
    + {["Physical SIM & eSIM", "Flexible data plans", "First month free"].map( + (feature, i) => ( +
  • + + {feature} +
  • + ) + )} +
+
+ View Plans +
-
-
- + + + {/* VPN Card */} + +
+
+
+
-
-
VPN
-
- Order immediately after signup. Router shipped upon order confirmation. -
+

+ VPN +

+

+ Access US/UK content with a pre-configured router. +

+
    + {[ + "Router rental included", + "San Francisco & London", + "Easy plug & play setup", + "Stream overseas content", + ].map((feature, i) => ( +
  • + + {feature} +
  • + ))} +
+
+ View Plans +
-
+
-
- } - features={[ - "Up to 10Gbps speeds", - "Fiber optic technology", - "Multiple access modes", - "Professional installation", - ]} - href={`${shopBasePath}/internet`} - color="blue" - /> - } - features={[ - "Physical SIM & eSIM", - "Data + SMS + Voice plans", - "Family discounts", - "Multiple data options", - ]} - href={`${shopBasePath}/sim`} - color="green" - /> - } - features={[ - "Secure encryption", - "Multiple locations", - "Business & personal", - "24/7 connectivity", - ]} - href={`${shopBasePath}/vpn`} - color="purple" - /> -
- -
-
-

- Why choose our services? -

-

- Reliable connectivity with transparent pricing and dedicated support. -

-
-
- } - title="Quality Networks" - description="NTT fiber for internet, 5G coverage for mobile, secure VPN infrastructure" - /> - } - title="Simple Management" - description="Manage all your services, billing, and support from one account portal" - /> -
+ {/* Help Link - Simple */} +
+

Need help choosing the right plan?

+ + Contact Us + +
); diff --git a/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx b/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx index 1bdea2ae..8c6ff870 100644 --- a/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx +++ b/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx @@ -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 (
- +
@@ -38,7 +38,7 @@ export function PublicInternetConfigureView() { return (
- + {/* Header */}
diff --git a/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx b/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx index f72fc524..b35b2ab7 100644 --- a/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx +++ b/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx @@ -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(null); + const internetFeatures: HighlightFeature[] = [ + { + icon: , + title: "NTT Optical Fiber", + description: "Japan's most reliable network with speeds up to 10Gbps", + highlight: "99.9% uptime", + }, + { + icon: , + title: "IPv6/IPoE Ready", + description: "Next-gen protocol for congestion-free browsing", + highlight: "No peak-hour slowdowns", + }, + { + icon: , + title: "Full English Support", + description: "Native English service for setup, billing & technical help", + highlight: "No language barriers", + }, + { + icon: , + title: "One Bill, One Provider", + description: "NTT line + ISP + equipment bundled with simple billing", + highlight: "No hidden fees", + }, + { + icon: , + title: "On-site Support", + description: "Technicians can visit for installation & troubleshooting", + highlight: "Professional setup", + }, + { + icon: , + 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 (
- + We couldn't load internet plans. Please try again later. @@ -300,20 +356,18 @@ export function PublicInternetPlansView() { return (
{/* Back link */} - + {/* Hero - Clean and impactful */}

- Internet Service Plans + {heroTitle}

-

- NTT Optical Fiber with full English support -

+

{heroDescription}

- {/* Why choose us - 3 pillars */} - + {/* Service Highlights */} + {/* Connection types - no extra header text */}
@@ -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() {

Enter your address to see what's available at your location

- + {onCtaClick ? ( + + ) : ( + + )}
{/* 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 ; +} + // Helper functions function getSpeedBadge(offeringType: string): string { const speeds: Record = { diff --git a/apps/portal/src/features/catalog/views/PublicSimConfigure.tsx b/apps/portal/src/features/catalog/views/PublicSimConfigure.tsx index ecb64781..166cce40 100644 --- a/apps/portal/src/features/catalog/views/PublicSimConfigure.tsx +++ b/apps/portal/src/features/catalog/views/PublicSimConfigure.tsx @@ -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 (
- +
@@ -41,7 +41,7 @@ export function PublicSimConfigureView() { if (!plan) { return (
- + The selected plan could not be found. Please go back and select a plan. @@ -51,7 +51,7 @@ export function PublicSimConfigureView() { return (
- + {/* Header */}
@@ -144,15 +144,15 @@ export function PublicSimConfigureView() { )}
- {/* Quick order info */} -
+ {/* Order process info */} +
- +
-

Order today, get started fast

+

How ordering works

- 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.

diff --git a/apps/portal/src/features/catalog/views/PublicSimPlans.tsx b/apps/portal/src/features/catalog/views/PublicSimPlans.tsx index c8203629..9986529a 100644 --- a/apps/portal/src/features/catalog/views/PublicSimPlans.tsx +++ b/apps/portal/src/features/catalog/views/PublicSimPlans.tsx @@ -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 ( +
+ +
+
{children}
+
+
+ ); +} + +// 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 ( +
+ {/* Data Size Badge */} +
+
+
+ +
+ {plan.simDataSize} +
+
+ + {/* Price */} +
+ +
+ + {/* Plan name */} +

{plan.name}

+ + {/* CTA */} + +
+ ); +} + /** * 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: , + title: "NTT Docomo Network", + description: "Best area coverage among the main three carriers in Japan", + highlight: "Nationwide coverage", + }, + { + icon: , + title: "First Month Free", + description: "Basic fee waived on signup to get you started risk-free", + highlight: "Great value", + }, + { + icon: , + title: "Foreign Cards Accepted", + description: "We accept both foreign and Japanese credit cards", + highlight: "No hassle", + }, + { + icon: , + title: "No Binding Contract", + description: "Minimum 4 months service (1st month free + 3 billing months)", + highlight: "Flexible contract", + }, + { + icon: , + title: "Number Portability", + description: "Easily switch to us keeping your current Japanese number", + highlight: "Keep your number", + }, + { + icon: , + title: "Free Plan Changes", + description: "Switch data plans anytime for the next billing cycle", + highlight: "Flexibility", + }, + ]; + if (isLoading) { return ( -
- - -
+
+ +
- +
- -
- -
- -
- {Array.from({ length: 6 }).map((_, i) => ( -
- - +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
))} @@ -73,15 +199,10 @@ export function PublicSimPlansView() { const errorMessage = error instanceof Error ? error.message : "An unexpected error occurred"; return (
-
-
Failed to load SIM plans
-
{errorMessage}
-
@@ -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 (
- + - - {/* Order info banner */} -
-
- -

- Order today - - {" "} - — eSIM activates instantly, physical SIM ships next business day. - -

-
+ {/* Hero Section - Clean & Minimal */} +
+
+ + Powered by NTT DOCOMO
- +

Mobile SIM Plans

+

+ Get connected with Japan's best network coverage. Choose eSIM for quick digital delivery + or physical SIM shipped to your door. +

+
-
-
- + {/* Service Highlights */} + + + {/* Plan Type Tabs */} +
+
+ + +
-
- {activeTab === "data-voice" && ( -
- } - plans={plansByType.DataSmsVoice} - showFamilyDiscount={false} - cardAction={plan => ({ label: "Continue", href: buildRedirect(plan.sku) })} - /> + {/* Plan Cards Grid */} +
+ {currentPlans.length > 0 ? ( +
+ {currentPlans.map(plan => ( + + ))}
- )} - {activeTab === "data-only" && ( -
- } - plans={plansByType.DataOnly} - showFamilyDiscount={false} - cardAction={plan => ({ label: "Continue", href: buildRedirect(plan.sku) })} - /> -
- )} - {activeTab === "voice-only" && ( -
- } - plans={plansByType.VoiceOnly} - showFamilyDiscount={false} - cardAction={plan => ({ label: "Continue", href: buildRedirect(plan.sku) })} - /> + ) : ( +
+ No plans available in this category.
)}
-
-

- Plan Features & Terms -

-
-
- + {/* Collapsible Information Sections */} +
+ {/* Calling & SMS Rates */} + +
+ {/* Domestic Rates */}
-
3-Month Contract
-
Minimum 3 billing months
+

+ + + + Domestic (Japan) +

+
+
+
Voice Calls
+
+ ¥10/30 sec +
+
+
+
SMS
+
+ ¥3/message +
+
+
+

+ Incoming calls and SMS are free. Pay-per-use charges billed 5-6 weeks after usage. +

+
+ + {/* Unlimited Option */} +
+
+ +
+

Unlimited Domestic Calling

+

+ Add unlimited domestic calls for{" "} + ¥3,000/month (available at + checkout) +

+
+
+
+ + {/* International Note */} +
+

+ International calling rates vary by country (¥31-148/30 sec). See{" "} + + NTT Docomo's website + {" "} + for full details. +

-
- + + + {/* Fees & Discounts */} + +
+ {/* Fees */}
-
First Month Free
-
Basic fee waived initially
+

One-time Fees

+
+
+ Activation Fee + ¥1,500 +
+
+ SIM Replacement (lost/damaged) + ¥1,500 +
+
+ eSIM Re-download + ¥1,500 +
+
+
+ + {/* Discounts */} +
+

Family Discount

+

+ ¥300/month off per additional + Voice SIM on your account +

+
+ +

All prices exclude 10% consumption tax.

+
+
+ + {/* Important Information & Terms */} + +
+ {/* Key Notices */} +
+

+ + Important Notices +

+
    +
  • + + + ID verification with official documents (name, date of birth, address, photo) is + required during checkout. + +
  • +
  • + + + A compatible unlocked device is required. Check compatibility on our website. + +
  • +
  • + + + Service may not be available in areas with weak signal. See{" "} + + NTT Docomo coverage map + + . + +
  • +
  • + + + SIM is activated as 4G by default. 5G can be requested via your account portal. + +
  • +
  • + + + International data roaming is not available. Voice/SMS roaming can be enabled + upon request (¥50,000/month limit). + +
  • +
+
+ + {/* Contract Terms */} +
+

Contract Terms

+
    +
  • + + + Minimum contract: 3 full billing + months. First month (sign-up to end of month) is free and doesn't count. + +
  • +
  • + + + Billing cycle: 1st to end of month. + Regular billing starts the 1st of the following month after sign-up. + +
  • +
  • + + + Cancellation: Can be requested + after 3rd month via cancellation form. Monthly fee is incurred in full for + cancellation month. + +
  • +
  • + + + SIM return: SIM card must be + returned after service termination. + +
  • +
+
+ + {/* Additional Options */} +
+

Additional Options

+
    +
  • + + Call waiting and voice mail available as separate paid options. +
  • +
  • + + Data plan changes are free and take effect next billing month. +
  • +
  • + + + Voice plan changes require new SIM issuance and standard policies apply. + +
  • +
+
+ + {/* Disclaimer */} +
+

+ 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. +

-
- -
-
5G Network
-
High-speed coverage
-
-
-
- -
-
eSIM Support
-
Digital activation
-
-
-
- -
-
Family Discounts
-
Multi-line savings (after sign-in)
-
-
-
- -
-
Plan Switching
-
Free data plan changes
-
-
-
+
- -
-
-
-
Contract Period
-

- Minimum 3 full billing months required. First month (sign-up to end of month) is - free and doesn't count toward contract. -

-
-
-
Billing Cycle
-

- Monthly billing from 1st to end of month. Regular billing starts on 1st of following - month after sign-up. -

-
-
-
-
-
Plan Changes
-

- Data plan switching is free and takes effect next month. Voice plan changes require - new SIM. -

-
-
-
SIM Replacement
-

Reissue fee of 1,500 JPY applies for damaged, lost, or replacement SIM cards.

-
-
-
-
+ {/* Terms Footer */} +
+

+ All prices exclude 10% consumption tax.{" "} + + View full Terms of Service + +

+
); } diff --git a/apps/portal/src/features/catalog/views/PublicVpnPlans.tsx b/apps/portal/src/features/catalog/views/PublicVpnPlans.tsx index 67962259..2350c9ba 100644 --- a/apps/portal/src/features/catalog/views/PublicVpnPlans.tsx +++ b/apps/portal/src/features/catalog/views/PublicVpnPlans.tsx @@ -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 (
- + - + + +
+
{children}
+
+
+ ); +} + +// 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 ( +
+ {/* Family Discount Badge */} + {isFamily && ( +
+ + Family Discount +
+ )} + + {/* Data Size Badge */} +
+
+
+ +
+ {plan.simDataSize} +
+
+ + {/* Price */} +
+ + {isFamily && ( +
Discounted price applied
+ )} +
+ + {/* Plan name */} +

{plan.name}

+ + {/* CTA */} + +
+ ); +} + 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(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: , + title: "NTT Docomo Network", + description: "Best area coverage among the main three carriers in Japan", + highlight: "Nationwide coverage", + }, + { + icon: , + title: "First Month Free", + description: "Basic fee waived on signup to get you started risk-free", + highlight: "Great value", + }, + { + icon: , + title: "Foreign Cards Accepted", + description: "We accept both foreign and Japanese credit cards", + highlight: "No hassle", + }, + { + icon: , + title: "No Binding Contract", + description: "Minimum 4 months service (1st month free + 3 billing months)", + highlight: "Flexible contract", + }, + { + icon: , + title: "Number Portability", + description: "Easily switch to us keeping your current Japanese number", + highlight: "Keep your number", + }, + { + icon: , + title: "Free Plan Changes", + description: "Switch data plans anytime for the next billing cycle", + highlight: "Flexibility", + }, + ]; + if (isLoading) { return ( -
- } - > -
- - - {/* Title block */} -
-
-
-
- - {/* Family discount banner slot */} -
-
-
- - {/* Tabs */} -
-
-
- - {/* Plans grid */} -
- {Array.from({ length: 6 }).map((_, i) => ( -
- - - -
- ))} -
- - {/* Terms section */} -
-
- {Array.from({ length: 6 }).map((_, i) => ( -
-
-
- - -
-
- ))} -
-
- - {/* Important terms banner */} -
+ } + > +
+ +
+ +
- -
+
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + + + +
+ ))} +
+
+ ); } @@ -116,15 +235,10 @@ export function SimPlansContainer() { description="Error loading plans" icon={} > -
-
Failed to load SIM plans
-
{errorMessage}
-
@@ -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 ( -
- } - > -
- + } + > +
+ - + {/* Hero Section */} +
+
+ + Powered by NTT DOCOMO +
+

+ Choose Your SIM Plan +

+

+ Get connected with Japan's best network coverage. Choose eSIM for quick digital delivery + or physical SIM shipped to your door. +

+
- {paymentMethodsLoading || residenceCardLoading ? ( - -

- Loading your payment method and residence card verification status. -

+ {/* Requirements Banners */} +
+ {paymentMethodsLoading ? ( + +

Loading your payment method status.

) : ( <> {paymentMethods && paymentMethods.totalCount === 0 && ( - +

SIM orders require a saved payment method on your account. @@ -187,312 +322,305 @@ export function SimPlansContainer() {

)} - - {residenceCard?.status === "pending" && ( - -

- We’re verifying your residence card. We’ll update your account once review is - complete. -

-
- )} - - {(residenceCard?.status === "not_submitted" || - residenceCard?.status === "rejected") && ( - -
-

- To order SIM service, please upload your residence card for identity - verification. -

-
- 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" - /> - -
- {submitResidenceCard.isError && ( -

- {submitResidenceCard.error instanceof Error - ? submitResidenceCard.error.message - : "Failed to submit residence card."} -

- )} -
-
- )} )} {hasExistingSim && ( - -
-

- You already have a SIM subscription with us. Family discount pricing is - automatically applied to eligible additional lines below. -

-
    -
  • Reduced monthly pricing automatically reflected
  • -
  • Same great features
  • -
  • Easy to manage multiple lines
  • -
-
+ +

+ You already have a SIM subscription. Discounted pricing is automatically shown for + additional lines. +

)} - -
-
- -
-
- -
- {activeTab === "data-voice" && ( -
- } - plans={plansByType.DataSmsVoice} - showFamilyDiscount={hasExistingSim} - /> -
- )} - {activeTab === "data-only" && ( -
- } - plans={plansByType.DataOnly} - showFamilyDiscount={hasExistingSim} - /> -
- )} - {activeTab === "voice-only" && ( -
- } - plans={plansByType.VoiceOnly} - showFamilyDiscount={hasExistingSim} - /> -
- )} -
- -
-

- Plan Features & Terms -

-
-
- -
-
3-Month Contract
-
Minimum 3 billing months
-
-
-
- -
-
First Month Free
-
Basic fee waived initially
-
-
-
- -
-
5G Network
-
High-speed coverage
-
-
-
- -
-
eSIM Support
-
Digital activation
-
-
-
- -
-
Family Discounts
-
Multi-line savings
-
-
-
- -
-
Plan Switching
-
Free data plan changes
-
-
-
-
- - -
-
-
-
Contract Period
-

- Minimum 3 full billing months required. First month (sign-up to end of month) is - free and doesn't count toward contract. -

-
-
-
Billing Cycle
-

- Monthly billing from 1st to end of month. Regular billing starts on 1st of - following month after sign-up. -

-
-
-
Cancellation
-

- Can be requested online after 3rd month. Service terminates at end of billing - cycle. -

-
-
-
-
-
Plan Changes
-

- Data plan switching is free and takes effect next month. Voice plan changes - require new SIM and cancellation policies apply. -

-
-
-
Calling/SMS Charges
-

- Pay-per-use charges apply separately. Billed 5-6 weeks after usage within - billing cycle. -

-
-
-
SIM Replacement
-

- Reissue fee of 1,500 JPY applies for damaged, lost, or replacement SIM cards. -

-
-
-
-
- -
+ + {/* Service Highlights (Shared with Public View) */} + + + {/* Plan Type Tabs */} +
+
+ + + +
+
+ + {/* Plan Cards Grid */} +
+ {regularPlans.length > 0 || familyPlans.length > 0 ? ( +
+ {/* Regular Plans */} + {regularPlans.length > 0 && ( +
+ {regularPlans.map(plan => ( + + ))} +
+ )} + + {/* Family Discount Plans */} + {hasExistingSim && familyPlans.length > 0 && ( +
+
+ +

Family Discount Plans

+
+
+ {familyPlans.map(plan => ( + + ))} +
+
+ )} +
+ ) : ( +
+ No plans available in this category. +
+ )} +
+ + {/* Collapsible Information Sections */} +
+ {/* Calling & SMS Rates */} + +
+ {/* Domestic Rates */} +
+

+ + + + Domestic (Japan) +

+
+
+
Voice Calls
+
+ ¥10/30 sec +
+
+
+
SMS
+
+ ¥3/message +
+
+
+

+ Incoming calls and SMS are free. Pay-per-use charges billed 5-6 weeks after usage. +

+
+ + {/* Unlimited Option */} +
+
+ +
+

Unlimited Domestic Calling

+

+ Add unlimited domestic calls for{" "} + ¥3,000/month (available at + checkout) +

+
+
+
+ + {/* International Note */} +
+

+ International calling rates vary by country (¥31-148/30 sec). See{" "} + + NTT Docomo's website + {" "} + for full details. +

+
+
+
+ + {/* Fees & Discounts */} + +
+ {/* Fees */} +
+

One-time Fees

+
+
+ Activation Fee + ¥1,500 +
+
+ SIM Replacement (lost/damaged) + ¥1,500 +
+
+ eSIM Re-download + ¥1,500 +
+
+
+ + {/* Discounts */} +
+

Family Discount

+

+ ¥300/month off per additional + Voice SIM on your account +

+
+ +

+ All prices exclude 10% consumption tax. +

+
+
+ + {/* Important Information & Terms */} + +
+ {/* Key Notices */} +
+

+ + Important Notices +

+
    +
  • + + + ID verification with official documents is required during checkout. + +
  • +
  • + + + A compatible unlocked device is required. Check compatibility on our website. + +
  • +
  • + + + SIM is activated as 4G by default. 5G can be requested via your account + portal. + +
  • +
  • + + + International data roaming is not available. Voice/SMS roaming can be enabled + upon request. + +
  • +
+
+ + {/* Contract Terms */} +
+

Contract Terms

+
    +
  • + + + Minimum contract: 3 full billing + months. + +
  • +
  • + + + Cancellation: Can be requested + after 3rd month via cancellation form. + +
  • +
  • + + + SIM return: SIM card must be + returned after service termination. + +
  • +
+
+ + {/* Disclaimer */} +
+

+ 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. +

+
+
+
+
+ + {/* Terms Footer */} +
+

+ All prices exclude 10% consumption tax.{" "} + + View full Terms of Service + +

+
+
+
); } diff --git a/apps/portal/src/features/catalog/views/VpnPlans.tsx b/apps/portal/src/features/catalog/views/VpnPlans.tsx index f95d3ae4..27b5c042 100644 --- a/apps/portal/src/features/catalog/views/VpnPlans.tsx +++ b/apps/portal/src/features/catalog/views/VpnPlans.tsx @@ -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={} >
- + } >
- + { @@ -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(null); const residenceFileInputRef = useRef(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 (
@@ -411,340 +408,116 @@ export function AccountCheckoutContainer() { )} - {isSimOrder ? ( - } - right={ - residenceStatus === "verified" ? ( - - ) : residenceStatus === "pending" ? ( - - ) : residenceStatus === "rejected" ? ( - - ) : ( - - ) - } - > - {residenceCardQuery.isLoading ? ( -
- Checking residence card status… -
- ) : residenceCardQuery.isError ? ( - - - - ) : residenceStatus === "verified" ? ( -
- - Your identity verification is complete. - - - {residenceCardQuery.data?.filename || residenceCardQuery.data?.submittedAt ? ( -
-
- Submitted document -
- {residenceCardQuery.data?.filename ? ( -
- {residenceCardQuery.data.filename} - {typeof residenceCardQuery.data.sizeBytes === "number" && - residenceCardQuery.data.sizeBytes > 0 ? ( - - {" "} - ·{" "} - {Math.round( - (residenceCardQuery.data.sizeBytes / 1024 / 1024) * 10 - ) / 10} - {" MB"} - - ) : null} -
- ) : null} -
- {formatDateTime(residenceCardQuery.data?.submittedAt) ? ( -
- Submitted: {formatDateTime(residenceCardQuery.data?.submittedAt)} -
- ) : null} - {formatDateTime(residenceCardQuery.data?.reviewedAt) ? ( -
- Reviewed: {formatDateTime(residenceCardQuery.data?.reviewedAt)} -
- ) : null} -
-
- ) : null} - -
- - Replace residence card - -
-

- Replacing the file restarts the verification process. -

- 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 ? ( -
-
-
- Selected file -
-
- {residenceFile.name} -
-
- -
- ) : null} - -
- -
- - {submitResidenceCard.isError && ( -
- {submitResidenceCard.error instanceof Error - ? submitResidenceCard.error.message - : "Failed to submit residence card."} -
- )} -
-
-
+ } + right={ + residenceStatus === "verified" ? ( + ) : residenceStatus === "pending" ? ( -
- - We’ll verify your residence card before activating SIM service. - + + ) : residenceStatus === "rejected" ? ( + + ) : ( + + ) + } + > + {residenceCardQuery.isLoading ? ( +
Checking residence card status…
+ ) : residenceCardQuery.isError ? ( + + + + ) : residenceStatus === "verified" ? ( +
+ + Your identity verification is complete. + - {residenceCardQuery.data?.filename || residenceCardQuery.data?.submittedAt ? ( -
-
- Submitted document -
- {residenceCardQuery.data?.filename ? ( -
- {residenceCardQuery.data.filename} - {typeof residenceCardQuery.data.sizeBytes === "number" && - residenceCardQuery.data.sizeBytes > 0 ? ( - - {" "} - ·{" "} - {Math.round( - (residenceCardQuery.data.sizeBytes / 1024 / 1024) * 10 - ) / 10} - {" MB"} - - ) : null} -
- ) : null} -
- {formatDateTime(residenceCardQuery.data?.submittedAt) ? ( -
- Submitted: {formatDateTime(residenceCardQuery.data?.submittedAt)} -
+ {residenceCardQuery.data?.filename || residenceCardQuery.data?.submittedAt ? ( +
+
+ Submitted document +
+ {residenceCardQuery.data?.filename ? ( +
+ {residenceCardQuery.data.filename} + {typeof residenceCardQuery.data.sizeBytes === "number" && + residenceCardQuery.data.sizeBytes > 0 ? ( + + {" "} + ·{" "} + {Math.round((residenceCardQuery.data.sizeBytes / 1024 / 1024) * 10) / + 10} + {" MB"} + ) : null}
-
- ) : null} - -
- - Replace residence card - -
-

- If you uploaded the wrong file, you can replace it. This restarts the - review. -

- 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 ? ( -
-
-
- Selected file -
-
- {residenceFile.name} -
-
- + ) : null} +
+ {formatDateTime(residenceCardQuery.data?.submittedAt) ? ( +
+ Submitted: {formatDateTime(residenceCardQuery.data?.submittedAt)}
) : null} + {formatDateTime(residenceCardQuery.data?.reviewedAt) ? ( +
Reviewed: {formatDateTime(residenceCardQuery.data?.reviewedAt)}
+ ) : null} +
+
+ ) : null} -
+
+ + Replace residence card + +
+

+ Replacing the file restarts the verification process. +

+ 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 ? ( +
+
+
+ Selected file +
+
+ {residenceFile.name} +
+
- - {submitResidenceCard.isError && ( -
- {submitResidenceCard.error instanceof Error - ? submitResidenceCard.error.message - : "Failed to submit residence card."} -
- )} -
-
-
- ) : ( - -
- {residenceStatus === "rejected" && residenceCardQuery.data?.reviewerNotes ? ( -
-
Rejection note
-
{residenceCardQuery.data.reviewerNotes}
-
- ) : residenceStatus === "rejected" ? ( -

- Your document couldn’t be approved. Please upload a new file to continue. -

) : null} -

- Upload a JPG, PNG, or PDF (max 5MB). We’ll verify it before activating SIM - service. -

-
- 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 ? ( -
-
-
- Selected file -
-
- {residenceFile.name} -
-
- -
- ) : null} -
- -
+
@@ -776,10 +548,218 @@ export function AccountCheckoutContainer() {
)}
+
+
+ ) : residenceStatus === "pending" ? ( +
+ + We’ll verify your residence card before activating SIM service. - )} - - ) : null} + + {residenceCardQuery.data?.filename || residenceCardQuery.data?.submittedAt ? ( +
+
+ Submitted document +
+ {residenceCardQuery.data?.filename ? ( +
+ {residenceCardQuery.data.filename} + {typeof residenceCardQuery.data.sizeBytes === "number" && + residenceCardQuery.data.sizeBytes > 0 ? ( + + {" "} + ·{" "} + {Math.round((residenceCardQuery.data.sizeBytes / 1024 / 1024) * 10) / + 10} + {" MB"} + + ) : null} +
+ ) : null} +
+ {formatDateTime(residenceCardQuery.data?.submittedAt) ? ( +
+ Submitted: {formatDateTime(residenceCardQuery.data?.submittedAt)} +
+ ) : null} +
+
+ ) : null} + +
+ + Replace residence card + +
+

+ If you uploaded the wrong file, you can replace it. This restarts the + review. +

+ 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 ? ( +
+
+
+ Selected file +
+
+ {residenceFile.name} +
+
+ +
+ ) : null} + +
+ +
+ + {submitResidenceCard.isError && ( +
+ {submitResidenceCard.error instanceof Error + ? submitResidenceCard.error.message + : "Failed to submit residence card."} +
+ )} +
+
+
+ ) : ( + +
+ {residenceStatus === "rejected" && residenceCardQuery.data?.reviewerNotes ? ( +
+
Rejection note
+
{residenceCardQuery.data.reviewerNotes}
+
+ ) : residenceStatus === "rejected" ? ( +

+ Your document couldn’t be approved. Please upload a new file to continue. +

+ ) : null} +

+ Upload a JPG, PNG, or PDF (max 5MB). We’ll verify it before activating SIM + service. +

+ +
+ 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 ? ( +
+
+
+ Selected file +
+
+ {residenceFile.name} +
+
+ +
+ ) : null} +
+ +
+ +
+ + {submitResidenceCard.isError && ( +
+ {submitResidenceCard.error instanceof Error + ? submitResidenceCard.error.message + : "Failed to submit residence card."} +
+ )} +
+
+ )} +
@@ -806,9 +786,7 @@ export function AccountCheckoutContainer() {

• Our team reviews your order and schedules setup if needed

• We may contact you to confirm details or availability

- {isSimOrder ? ( -

• For SIM orders, we verify your residence card before SIM activation

- ) : null} +

• We verify your residence card before service activation

• We only charge your card after the order is approved

• You’ll receive confirmation and next steps by email

diff --git a/apps/portal/src/features/checkout/components/CheckoutEntry.tsx b/apps/portal/src/features/checkout/components/CheckoutEntry.tsx index 8a844761..86390137 100644 --- a/apps/portal/src/features/checkout/components/CheckoutEntry.tsx +++ b/apps/portal/src/features/checkout/components/CheckoutEntry.tsx @@ -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 (
diff --git a/apps/portal/src/features/checkout/components/CheckoutErrorBoundary.tsx b/apps/portal/src/features/checkout/components/CheckoutErrorBoundary.tsx index 0b31edd9..4590996e 100644 --- a/apps/portal/src/features/checkout/components/CheckoutErrorBoundary.tsx +++ b/apps/portal/src/features/checkout/components/CheckoutErrorBoundary.tsx @@ -52,7 +52,7 @@ export class CheckoutErrorBoundary extends Component { > Try Again -
diff --git a/apps/portal/src/features/checkout/components/CheckoutStatusBanners.tsx b/apps/portal/src/features/checkout/components/CheckoutStatusBanners.tsx index e41cad74..0484bf29 100644 --- a/apps/portal/src/features/checkout/components/CheckoutStatusBanners.tsx +++ b/apps/portal/src/features/checkout/components/CheckoutStatusBanners.tsx @@ -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. -
diff --git a/apps/portal/src/features/checkout/components/EmptyCartRedirect.tsx b/apps/portal/src/features/checkout/components/EmptyCartRedirect.tsx index 0f582450..3cb3efa0 100644 --- a/apps/portal/src/features/checkout/components/EmptyCartRedirect.tsx +++ b/apps/portal/src/features/checkout/components/EmptyCartRedirect.tsx @@ -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 (
@@ -33,7 +33,7 @@ export function EmptyCartRedirect() {

Browse our services to find the perfect plan for your needs.

-

diff --git a/apps/portal/src/features/dashboard/components/TaskList.tsx b/apps/portal/src/features/dashboard/components/TaskList.tsx index b9971344..21006232 100644 --- a/apps/portal/src/features/dashboard/components/TaskList.tsx +++ b/apps/portal/src/features/dashboard/components/TaskList.tsx @@ -52,7 +52,7 @@ function AllCaughtUp() { {/* Quick action cards */}

diff --git a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx index 6d51d1d7..a3e948a6 100644 --- a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx +++ b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx @@ -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 ( -
- {/* Hero */} -
-
- {/* Subtle glow behind logo */} -
-
- +
+ {/* Hero Section */} +
+
+ +
+
+ + + + + Reliable Connectivity in Japan
-
-
-

- Account Portal +

+ A One Stop Solution + + for Your IT Needs +

-

- Manage your services, billing, and support in one place. +

+ Serving Japan's international community with reliable, English-supported internet, + mobile, and VPN solutions.

+
+ + Browse Services + + + + Contact Us + +
- {/* Browse services CTA - New prominent section */} -
-
-
-
-
- -
-
-

Browse Our Services

-

- Explore internet, SIM, and VPN plans — no account needed -

-
+ {/* CONCEPT Section - Value Propositions (ONLY on homepage) */} +
+
+

+ Our Concept +

+

+ Why customers choose us +

+
+
+ {/* One Stop Solution */} +
+
+
- - Shop Services - - +

One Stop Solution

+

+ All you need is just to contact us and we will take care of everything. +

+
+ + {/* English Support */} +
+
+ +
+

English Support

+

+ We always assist you in English. No language barrier to worry about. +

+
+ + {/* Onsite Support */} +
+
+ +
+

Onsite Support

+

+ Our tech staff can visit your residence for setup and troubleshooting. +

- {/* Service highlights */} -
-
+ {/* Services Teaser - Brief preview linking to /services */} +
+

+ Our Services +

+

What we offer

+
-
- -
-
+ + Internet -
-
Up to 10Gbps fiber
+ - -
- -
-
+ + SIM & eSIM -
-
Data, voice & SMS plans
+ - -
- -
-
+ + VPN -
-
Secure remote access
+ + + + + + Business + + + + + + Onsite Support + + + + + + TV Services +
+ + Browse all services + +
- {/* Primary actions */} -
-
-
-
-
- -
-
-

Existing customers

-

- Sign in or migrate your account from the old system. -

-
- - Sign in - - - - Migrate account - -
-
-
-
- -
-
-
-
- -
-
-

New customers

-

- Browse our services and sign up during checkout, or create an account first. -

-
- - Browse services - - - - Create account - -
-
-
-
-
- - {/* Feature highlights */} -
-
-
-
-

Everything you need

-

Powerful tools to manage your account

-
- - Need help? - - -
-
-
-
- -
-
Billing
-
- View invoices, payments, and billing history. -
-
- -
-
- -
-
Services
-
- Manage subscriptions and service details. -
-
- -
-
- -
-
Support
-
- Create cases and track responses in one place. -
+ {/* CTA Section */} +
+
+
+
+
+

+ Ready to get connected? +

+

+ Contact us anytime — our bilingual team is here to help you find the right solution. +

+
+ + Contact Us + + + + Browse Services +
diff --git a/apps/portal/src/features/orders/views/OrdersList.tsx b/apps/portal/src/features/orders/views/OrdersList.tsx index f6681c92..8aee7691 100644 --- a/apps/portal/src/features/orders/views/OrdersList.tsx +++ b/apps/portal/src/features/orders/views/OrdersList.tsx @@ -90,7 +90,7 @@ export function OrdersListContainer() { icon={} title="No orders yet" description="You haven't placed any orders yet." - action={{ label: "Browse Services", onClick: () => router.push("/shop") }} + action={{ label: "Browse Services", onClick: () => router.push("/account/services") }} /> ) : ( diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx index 230e6a32..ce5341fa 100644 --- a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx +++ b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx @@ -94,7 +94,7 @@ export function SubscriptionsListContainer() { title="Services" description="Manage your active services" actions={ - } diff --git a/apps/portal/src/features/support/views/PublicContactView.tsx b/apps/portal/src/features/support/views/PublicContactView.tsx index 4adf5905..6054aab8 100644 --- a/apps/portal/src/features/support/views/PublicContactView.tsx +++ b/apps/portal/src/features/support/views/PublicContactView.tsx @@ -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; /** - * 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() {

Message Sent!

- 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.

-
@@ -82,117 +82,224 @@ export function PublicContactView() { } return ( -
- {/* Back link */} - - - Back to Support - - +
{/* Header */} -
-
- +
+

+ Get in Touch +

+

+ Have a question about our services? We're here to help you find the perfect solution for + your stay in Japan. +

+
+ +
+ {/* Left Column - Contact Form */} +
+
+
+
+
+ +
+
+

Send a Message

+

+ We typically reply within 24 hours +

+
+
+
+ +
+ {submitError && ( + + {submitError} + + )} + +
void form.handleSubmit(event)} className="space-y-6"> +
+ + form.setValue("name", e.target.value)} + onBlur={() => form.setTouchedField("name")} + placeholder="Your name" + className="bg-background" + /> + + + + form.setValue("email", e.target.value)} + onBlur={() => form.setTouchedField("email")} + placeholder="your@email.com" + className="bg-background" + /> + +
+ +
+ + form.setValue("phone", e.target.value)} + onBlur={() => form.setTouchedField("phone")} + placeholder="+81 90-1234-5678" + className="bg-background" + /> + + + + form.setValue("subject", e.target.value)} + onBlur={() => form.setTouchedField("subject")} + placeholder="How can we help?" + className="bg-background" + /> + +
+ + +