2025-09-17 18:43:43 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2025-12-22 18:59:38 +09:00
|
|
|
import { useEffect, useMemo, useState } from "react";
|
2025-12-24 19:01:21 +09:00
|
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
2025-12-25 11:54:33 +09:00
|
|
|
import { Server, CheckCircle, Clock, TriangleAlert, MapPin } from "lucide-react";
|
2025-12-25 13:20:45 +09:00
|
|
|
import { useInternetCatalog } from "@/features/services/hooks";
|
2025-09-17 18:43:43 +09:00
|
|
|
import { useActiveSubscriptions } from "@/features/subscriptions/hooks/useSubscriptions";
|
2025-09-19 16:34:10 +09:00
|
|
|
import type {
|
2025-09-24 18:00:49 +09:00
|
|
|
InternetPlanCatalogItem,
|
|
|
|
|
InternetInstallationCatalogItem,
|
2025-12-25 13:20:45 +09:00
|
|
|
} from "@customer-portal/domain/services";
|
2025-10-22 11:33:23 +09:00
|
|
|
import { Skeleton } from "@/components/atoms/loading-skeleton";
|
2025-09-25 15:54:54 +09:00
|
|
|
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
|
|
|
|
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
2025-12-17 17:59:55 +09:00
|
|
|
import { Button } from "@/components/atoms/button";
|
2025-12-25 13:20:45 +09:00
|
|
|
import { CatalogBackLink } from "@/features/services/components/base/CatalogBackLink";
|
|
|
|
|
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
|
|
|
|
import { InternetImportantNotes } from "@/features/services/components/internet/InternetImportantNotes";
|
2025-12-24 13:24:13 +09:00
|
|
|
import {
|
|
|
|
|
InternetOfferingCard,
|
|
|
|
|
type TierInfo,
|
2025-12-25 13:20:45 +09:00
|
|
|
} from "@/features/services/components/internet/InternetOfferingCard";
|
|
|
|
|
import { PublicInternetPlansContent } from "@/features/services/views/PublicInternetPlans";
|
|
|
|
|
import { PlanComparisonGuide } from "@/features/services/components/internet/PlanComparisonGuide";
|
2025-12-17 17:59:55 +09:00
|
|
|
import {
|
|
|
|
|
useInternetEligibility,
|
|
|
|
|
useRequestInternetEligibilityCheck,
|
2025-12-25 13:20:45 +09:00
|
|
|
} from "@/features/services/hooks";
|
2025-12-17 17:59:55 +09:00
|
|
|
import { useAuthSession } from "@/features/auth/services/auth.store";
|
2025-12-24 13:24:13 +09:00
|
|
|
import { cn } from "@/lib/utils";
|
2025-09-17 18:43:43 +09:00
|
|
|
|
2025-12-22 18:59:38 +09:00
|
|
|
type AutoRequestStatus = "idle" | "submitting" | "submitted" | "failed" | "missing_address";
|
|
|
|
|
|
2025-12-24 13:24:13 +09:00
|
|
|
// Offering configuration for display
|
|
|
|
|
interface OfferingConfig {
|
|
|
|
|
offeringType: string;
|
|
|
|
|
title: string;
|
|
|
|
|
speedBadge: string;
|
|
|
|
|
description: string;
|
|
|
|
|
iconType: "home" | "apartment";
|
|
|
|
|
isPremium: boolean;
|
|
|
|
|
displayOrder: number;
|
|
|
|
|
isAlternative?: boolean;
|
|
|
|
|
alternativeNote?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const OFFERING_CONFIGS: Record<string, Omit<OfferingConfig, "offeringType">> = {
|
|
|
|
|
"Home 10G": {
|
|
|
|
|
title: "Home 10Gbps",
|
|
|
|
|
speedBadge: "10 Gbps",
|
|
|
|
|
description: "Ultra-fast fiber with the highest speeds available in Japan.",
|
|
|
|
|
iconType: "home",
|
|
|
|
|
isPremium: true,
|
|
|
|
|
displayOrder: 1,
|
|
|
|
|
},
|
|
|
|
|
"Home 1G": {
|
|
|
|
|
title: "Home 1Gbps",
|
|
|
|
|
speedBadge: "1 Gbps",
|
|
|
|
|
description: "High-speed fiber. The most popular choice for home internet.",
|
|
|
|
|
iconType: "home",
|
|
|
|
|
isPremium: false,
|
|
|
|
|
displayOrder: 2,
|
|
|
|
|
},
|
|
|
|
|
"Apartment 1G": {
|
|
|
|
|
title: "Apartment 1Gbps",
|
|
|
|
|
speedBadge: "1 Gbps",
|
|
|
|
|
description: "High-speed fiber-to-the-unit for mansions and apartment buildings.",
|
|
|
|
|
iconType: "apartment",
|
|
|
|
|
isPremium: false,
|
|
|
|
|
displayOrder: 1,
|
|
|
|
|
},
|
|
|
|
|
"Apartment 100M": {
|
|
|
|
|
title: "Apartment 100Mbps",
|
|
|
|
|
speedBadge: "100 Mbps",
|
2025-12-24 19:01:21 +09:00
|
|
|
description: "Standard speed via VDSL or LAN for apartment buildings.",
|
2025-12-24 13:24:13 +09:00
|
|
|
iconType: "apartment",
|
|
|
|
|
isPremium: false,
|
|
|
|
|
displayOrder: 2,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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<
|
|
|
|
|
string,
|
|
|
|
|
{ description: string; features: string[]; pricingNote?: string }
|
|
|
|
|
> = {
|
|
|
|
|
Silver: {
|
|
|
|
|
description: "Essential setup—bring your own router",
|
2025-12-24 19:01:21 +09:00
|
|
|
features: ["NTT modem + ISP connection", "IPoE or PPPoE protocols", "Self-configuration"],
|
2025-12-24 13:24:13 +09:00
|
|
|
},
|
|
|
|
|
Gold: {
|
|
|
|
|
description: "All-inclusive with router rental",
|
|
|
|
|
features: [
|
2025-12-24 19:01:21 +09:00
|
|
|
"Everything in Silver",
|
2025-12-24 13:24:13 +09:00
|
|
|
"WiFi router included",
|
2025-12-24 19:01:21 +09:00
|
|
|
"Auto-configured",
|
|
|
|
|
"Range extender option",
|
2025-12-24 13:24:13 +09:00
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
Platinum: {
|
2025-12-24 19:01:21 +09:00
|
|
|
description: "Tailored setup for larger homes",
|
2025-12-24 13:24:13 +09:00
|
|
|
features: [
|
|
|
|
|
"Netgear INSIGHT mesh routers",
|
2025-12-24 19:01:21 +09:00
|
|
|
"Cloud-managed WiFi",
|
|
|
|
|
"Remote support",
|
|
|
|
|
"Custom setup",
|
2025-12-24 13:24:13 +09:00
|
|
|
],
|
2025-12-24 19:01:21 +09:00
|
|
|
pricingNote: "+ equipment fees",
|
2025-12-24 13:24:13 +09:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
description: config.description,
|
|
|
|
|
features: config.features,
|
|
|
|
|
recommended: tier === "Gold",
|
|
|
|
|
pricingNote: config.pricingNote,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSetupFee(installations: InternetInstallationCatalogItem[]): number {
|
|
|
|
|
const basic = installations.find(i => i.sku?.toLowerCase().includes("basic"));
|
|
|
|
|
return basic?.oneTimePrice ?? 22800;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getAvailableOfferings(
|
|
|
|
|
eligibility: string | null,
|
|
|
|
|
plans: InternetPlanCatalogItem[]
|
|
|
|
|
): OfferingConfig[] {
|
|
|
|
|
if (!eligibility) return [];
|
|
|
|
|
|
|
|
|
|
const results: OfferingConfig[] = [];
|
|
|
|
|
const eligibilityLower = eligibility.toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (eligibilityLower.includes("home 10g")) {
|
|
|
|
|
const config10g = OFFERING_CONFIGS["Home 10G"];
|
|
|
|
|
const config1g = OFFERING_CONFIGS["Home 1G"];
|
|
|
|
|
if (config10g && plans.some(p => p.internetOfferingType === "Home 10G")) {
|
2025-12-24 19:01:21 +09:00
|
|
|
results.push({ offeringType: "Home 10G", ...config10g });
|
2025-12-24 13:24:13 +09:00
|
|
|
}
|
|
|
|
|
if (config1g && plans.some(p => p.internetOfferingType === "Home 1G")) {
|
|
|
|
|
results.push({
|
|
|
|
|
offeringType: "Home 1G",
|
|
|
|
|
...config1g,
|
|
|
|
|
isAlternative: true,
|
2025-12-24 19:01:21 +09:00
|
|
|
alternativeNote: "Lower monthly cost option",
|
2025-12-24 13:24:13 +09:00
|
|
|
});
|
|
|
|
|
}
|
2025-12-24 19:01:21 +09:00
|
|
|
} else if (eligibilityLower.includes("home 1g")) {
|
2025-12-24 13:24:13 +09:00
|
|
|
const config = OFFERING_CONFIGS["Home 1G"];
|
|
|
|
|
if (config && plans.some(p => p.internetOfferingType === "Home 1G")) {
|
2025-12-24 19:01:21 +09:00
|
|
|
results.push({ offeringType: "Home 1G", ...config });
|
2025-12-24 13:24:13 +09:00
|
|
|
}
|
2025-12-24 19:01:21 +09:00
|
|
|
} else if (eligibilityLower.includes("apartment 1g")) {
|
2025-12-24 13:24:13 +09:00
|
|
|
const config = OFFERING_CONFIGS["Apartment 1G"];
|
|
|
|
|
if (config && plans.some(p => p.internetOfferingType === "Apartment 1G")) {
|
2025-12-24 19:01:21 +09:00
|
|
|
results.push({ offeringType: "Apartment 1G", ...config });
|
2025-12-24 13:24:13 +09:00
|
|
|
}
|
2025-12-24 19:01:21 +09:00
|
|
|
} else if (eligibilityLower.includes("apartment 100m")) {
|
2025-12-24 13:24:13 +09:00
|
|
|
const config = OFFERING_CONFIGS["Apartment 100M"];
|
|
|
|
|
if (config && plans.some(p => p.internetOfferingType === "Apartment 100M")) {
|
2025-12-24 19:01:21 +09:00
|
|
|
results.push({ offeringType: "Apartment 100M", ...config });
|
2025-12-24 13:24:13 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return results.sort((a, b) => a.displayOrder - b.displayOrder);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatEligibilityDisplay(eligibility: string): {
|
|
|
|
|
residenceType: "home" | "apartment";
|
|
|
|
|
speed: string;
|
|
|
|
|
label: string;
|
|
|
|
|
description: string;
|
|
|
|
|
} {
|
|
|
|
|
const lower = eligibility.toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (lower.includes("home 10g")) {
|
|
|
|
|
return {
|
|
|
|
|
residenceType: "home",
|
|
|
|
|
speed: "10 Gbps",
|
|
|
|
|
label: "Standalone House (10Gbps available)",
|
|
|
|
|
description:
|
|
|
|
|
"Your address supports our fastest 10Gbps service. You can also choose 1Gbps for lower monthly cost.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (lower.includes("home 1g")) {
|
|
|
|
|
return {
|
|
|
|
|
residenceType: "home",
|
|
|
|
|
speed: "1 Gbps",
|
|
|
|
|
label: "Standalone House (1Gbps)",
|
|
|
|
|
description: "Your address supports high-speed 1Gbps fiber connection.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (lower.includes("apartment 1g")) {
|
|
|
|
|
return {
|
|
|
|
|
residenceType: "apartment",
|
|
|
|
|
speed: "1 Gbps",
|
2025-12-24 19:01:21 +09:00
|
|
|
label: "Apartment/Mansion (1Gbps FTTH)",
|
2025-12-24 13:24:13 +09:00
|
|
|
description: "Your building has fiber-to-the-unit infrastructure supporting 1Gbps speeds.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (lower.includes("apartment 100m")) {
|
|
|
|
|
return {
|
|
|
|
|
residenceType: "apartment",
|
|
|
|
|
speed: "100 Mbps",
|
|
|
|
|
label: "Apartment/Mansion (100Mbps)",
|
|
|
|
|
description: "Your building uses VDSL or LAN infrastructure with up to 100Mbps speeds.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
residenceType: "home",
|
|
|
|
|
speed: eligibility,
|
|
|
|
|
label: eligibility,
|
|
|
|
|
description: "Service is available at your address.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 19:01:21 +09:00
|
|
|
// Status badge component
|
|
|
|
|
function EligibilityStatusBadge({
|
|
|
|
|
status,
|
|
|
|
|
speed,
|
|
|
|
|
}: {
|
|
|
|
|
status: "eligible" | "pending" | "not_requested" | "ineligible";
|
|
|
|
|
speed?: string;
|
|
|
|
|
}) {
|
|
|
|
|
const configs = {
|
|
|
|
|
eligible: {
|
2025-12-25 11:54:33 +09:00
|
|
|
icon: CheckCircle,
|
2025-12-24 19:01:21 +09:00
|
|
|
bg: "bg-success-soft",
|
|
|
|
|
border: "border-success/30",
|
|
|
|
|
text: "text-success",
|
|
|
|
|
label: "Service Available",
|
|
|
|
|
},
|
|
|
|
|
pending: {
|
2025-12-25 11:54:33 +09:00
|
|
|
icon: Clock,
|
2025-12-24 19:01:21 +09:00
|
|
|
bg: "bg-info-soft",
|
|
|
|
|
border: "border-info/30",
|
|
|
|
|
text: "text-info",
|
|
|
|
|
label: "Review in Progress",
|
|
|
|
|
},
|
|
|
|
|
not_requested: {
|
2025-12-25 11:54:33 +09:00
|
|
|
icon: MapPin,
|
2025-12-24 19:01:21 +09:00
|
|
|
bg: "bg-muted",
|
|
|
|
|
border: "border-border",
|
|
|
|
|
text: "text-muted-foreground",
|
|
|
|
|
label: "Verification Required",
|
|
|
|
|
},
|
|
|
|
|
ineligible: {
|
2025-12-25 11:54:33 +09:00
|
|
|
icon: TriangleAlert,
|
2025-12-24 19:01:21 +09:00
|
|
|
bg: "bg-warning/10",
|
|
|
|
|
border: "border-warning/30",
|
|
|
|
|
text: "text-warning",
|
|
|
|
|
label: "Not Available",
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const config = configs[status];
|
|
|
|
|
const Icon = config.icon;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"inline-flex items-center gap-2 px-4 py-2 rounded-full border",
|
|
|
|
|
config.bg,
|
|
|
|
|
config.border
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Icon className={cn("h-4 w-4", config.text)} />
|
|
|
|
|
<span className={cn("font-semibold text-sm", config.text)}>{config.label}</span>
|
|
|
|
|
{status === "eligible" && speed && (
|
|
|
|
|
<>
|
|
|
|
|
<span className="text-muted-foreground">·</span>
|
|
|
|
|
<span className="text-sm text-foreground font-medium">Up to {speed}</span>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-17 18:43:43 +09:00
|
|
|
export function InternetPlansContainer() {
|
2025-12-24 19:01:21 +09:00
|
|
|
const router = useRouter();
|
|
|
|
|
const servicesBasePath = useServicesBasePath();
|
2025-12-22 18:59:38 +09:00
|
|
|
const searchParams = useSearchParams();
|
2025-12-17 17:59:55 +09:00
|
|
|
const { user } = useAuthSession();
|
2025-09-17 18:43:43 +09:00
|
|
|
const { data, isLoading, error } = useInternetCatalog();
|
2025-12-17 17:59:55 +09:00
|
|
|
const eligibilityQuery = useInternetEligibility();
|
2025-12-22 18:59:38 +09:00
|
|
|
const eligibilityLoading = eligibilityQuery.isLoading;
|
|
|
|
|
const refetchEligibility = eligibilityQuery.refetch;
|
2025-12-17 17:59:55 +09:00
|
|
|
const eligibilityRequest = useRequestInternetEligibilityCheck();
|
2025-12-22 18:59:38 +09:00
|
|
|
const submitEligibilityRequest = eligibilityRequest.mutateAsync;
|
2025-10-22 11:55:47 +09:00
|
|
|
const plans: InternetPlanCatalogItem[] = useMemo(() => data?.plans ?? [], [data?.plans]);
|
2025-10-22 11:33:23 +09:00
|
|
|
const installations: InternetInstallationCatalogItem[] = useMemo(
|
|
|
|
|
() => data?.installations ?? [],
|
|
|
|
|
[data?.installations]
|
|
|
|
|
);
|
2025-09-17 18:43:43 +09:00
|
|
|
const { data: activeSubs } = useActiveSubscriptions();
|
2025-10-29 13:29:28 +09:00
|
|
|
const hasActiveInternet = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
Array.isArray(activeSubs)
|
|
|
|
|
? activeSubs.some(
|
|
|
|
|
s =>
|
|
|
|
|
String(s.productName || "")
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.includes("sonixnet via ntt optical fiber") &&
|
|
|
|
|
String(s.status || "").toLowerCase() === "active"
|
|
|
|
|
)
|
|
|
|
|
: false,
|
|
|
|
|
[activeSubs]
|
|
|
|
|
);
|
2025-09-17 18:43:43 +09:00
|
|
|
|
2025-12-17 17:59:55 +09:00
|
|
|
const eligibilityValue = eligibilityQuery.data?.eligibility;
|
2025-12-19 15:15:36 +09:00
|
|
|
const eligibilityStatus = eligibilityQuery.data?.status;
|
|
|
|
|
const requestedAt = eligibilityQuery.data?.requestedAt;
|
|
|
|
|
const rejectionNotes = eligibilityQuery.data?.notes;
|
|
|
|
|
|
|
|
|
|
const isEligible =
|
|
|
|
|
eligibilityStatus === "eligible" &&
|
|
|
|
|
typeof eligibilityValue === "string" &&
|
|
|
|
|
eligibilityValue.trim().length > 0;
|
|
|
|
|
const isPending = eligibilityStatus === "pending";
|
|
|
|
|
const isNotRequested = eligibilityStatus === "not_requested";
|
|
|
|
|
const isIneligible = eligibilityStatus === "ineligible";
|
2025-12-23 16:44:45 +09:00
|
|
|
|
2025-12-17 17:59:55 +09:00
|
|
|
const hasServiceAddress = Boolean(
|
|
|
|
|
user?.address?.address1 &&
|
|
|
|
|
user?.address?.city &&
|
|
|
|
|
user?.address?.postcode &&
|
|
|
|
|
(user?.address?.country || user?.address?.countryCode)
|
|
|
|
|
);
|
2025-12-22 18:59:38 +09:00
|
|
|
const autoEligibilityRequest = searchParams?.get("autoEligibilityRequest") === "1";
|
|
|
|
|
const autoPlanSku = searchParams?.get("planSku");
|
|
|
|
|
const [autoRequestStatus, setAutoRequestStatus] = useState<AutoRequestStatus>("idle");
|
|
|
|
|
const [autoRequestId, setAutoRequestId] = useState<string | null>(null);
|
2025-12-19 15:15:36 +09:00
|
|
|
const addressLabel = useMemo(() => {
|
|
|
|
|
const a = user?.address;
|
|
|
|
|
if (!a) return "";
|
|
|
|
|
return [a.address1, a.address2, a.city, a.state, a.postcode, a.country || a.countryCode]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.map(part => String(part).trim())
|
|
|
|
|
.filter(part => part.length > 0)
|
|
|
|
|
.join(", ");
|
|
|
|
|
}, [user?.address]);
|
2025-12-17 17:59:55 +09:00
|
|
|
|
2025-12-19 15:15:36 +09:00
|
|
|
const eligibility = useMemo(() => {
|
2025-12-24 13:24:13 +09:00
|
|
|
if (!isEligible) return null;
|
|
|
|
|
return eligibilityValue?.trim() ?? null;
|
2025-12-19 15:15:36 +09:00
|
|
|
}, [eligibilityValue, isEligible]);
|
2025-09-17 18:43:43 +09:00
|
|
|
|
2025-12-24 13:24:13 +09:00
|
|
|
const setupFee = useMemo(() => getSetupFee(installations), [installations]);
|
|
|
|
|
|
|
|
|
|
const availableOfferings = useMemo(() => {
|
|
|
|
|
if (!eligibility) return [];
|
|
|
|
|
return getAvailableOfferings(eligibility, plans);
|
|
|
|
|
}, [eligibility, plans]);
|
|
|
|
|
|
|
|
|
|
const eligibilityDisplay = useMemo(() => {
|
|
|
|
|
if (!eligibility) return null;
|
|
|
|
|
return formatEligibilityDisplay(eligibility);
|
|
|
|
|
}, [eligibility]);
|
|
|
|
|
|
|
|
|
|
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,
|
2025-12-24 19:01:21 +09:00
|
|
|
ctaPath: `${servicesBasePath}/internet/configure`,
|
2025-12-24 13:24:13 +09:00
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter(card => card.tiers.length > 0);
|
2025-12-24 19:01:21 +09:00
|
|
|
}, [availableOfferings, plans, setupFee, servicesBasePath]);
|
2025-12-24 13:24:13 +09:00
|
|
|
|
2025-12-24 19:01:21 +09:00
|
|
|
// 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
|
2025-12-22 18:59:38 +09:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!autoEligibilityRequest) return;
|
|
|
|
|
if (autoRequestStatus !== "idle") return;
|
|
|
|
|
if (eligibilityLoading) return;
|
|
|
|
|
if (!isNotRequested) {
|
2025-12-24 19:01:21 +09:00
|
|
|
router.replace(`${servicesBasePath}/internet`);
|
2025-12-22 18:59:38 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!hasServiceAddress) {
|
|
|
|
|
setAutoRequestStatus("missing_address");
|
2025-12-24 19:01:21 +09:00
|
|
|
router.replace(`${servicesBasePath}/internet`);
|
2025-12-22 18:59:38 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const submit = async () => {
|
|
|
|
|
setAutoRequestStatus("submitting");
|
|
|
|
|
try {
|
|
|
|
|
const notes = autoPlanSku
|
|
|
|
|
? `Requested after signup. Selected plan SKU: ${autoPlanSku}`
|
|
|
|
|
: "Requested after signup.";
|
|
|
|
|
const result = await submitEligibilityRequest({
|
|
|
|
|
address: user?.address ?? undefined,
|
|
|
|
|
notes,
|
|
|
|
|
});
|
|
|
|
|
setAutoRequestId(result.requestId ?? null);
|
|
|
|
|
setAutoRequestStatus("submitted");
|
|
|
|
|
await refetchEligibility();
|
2025-12-24 19:01:21 +09:00
|
|
|
} catch {
|
2025-12-22 18:59:38 +09:00
|
|
|
setAutoRequestStatus("failed");
|
|
|
|
|
} finally {
|
2025-12-24 19:01:21 +09:00
|
|
|
router.replace(`${servicesBasePath}/internet`);
|
2025-12-22 18:59:38 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void submit();
|
|
|
|
|
}, [
|
|
|
|
|
autoEligibilityRequest,
|
|
|
|
|
autoPlanSku,
|
|
|
|
|
autoRequestStatus,
|
|
|
|
|
eligibilityLoading,
|
|
|
|
|
refetchEligibility,
|
|
|
|
|
submitEligibilityRequest,
|
|
|
|
|
hasServiceAddress,
|
|
|
|
|
isNotRequested,
|
2025-12-24 19:01:21 +09:00
|
|
|
servicesBasePath,
|
2025-12-22 18:59:38 +09:00
|
|
|
user?.address,
|
2025-12-24 19:01:21 +09:00
|
|
|
router,
|
2025-12-22 18:59:38 +09:00
|
|
|
]);
|
|
|
|
|
|
2025-12-24 19:01:21 +09:00
|
|
|
// Loading state
|
2025-09-17 18:43:43 +09:00
|
|
|
if (isLoading || error) {
|
|
|
|
|
return (
|
2025-12-25 11:54:33 +09:00
|
|
|
<div className="max-w-4xl mx-auto px-4 pt-8">
|
2025-12-16 13:54:31 +09:00
|
|
|
<AsyncBlock isLoading={false} error={error}>
|
2025-12-24 13:24:13 +09:00
|
|
|
<div className="max-w-4xl mx-auto px-4">
|
2025-12-24 19:01:21 +09:00
|
|
|
<CatalogBackLink href={servicesBasePath} label="Back to Services" className="mb-4" />
|
2025-12-16 13:54:31 +09:00
|
|
|
<div className="text-center mb-12">
|
2025-12-24 13:24:13 +09:00
|
|
|
<Skeleton className="h-10 w-96 mx-auto mb-4" />
|
|
|
|
|
<Skeleton className="h-4 w-[32rem] max-w-full mx-auto" />
|
2025-12-16 13:54:31 +09:00
|
|
|
</div>
|
2025-12-24 13:24:13 +09:00
|
|
|
<div className="space-y-4">
|
2025-12-24 19:01:21 +09:00
|
|
|
{[1, 2].map(i => (
|
2025-10-22 10:58:16 +09:00
|
|
|
<div
|
2025-12-16 13:54:31 +09:00
|
|
|
key={i}
|
2025-12-24 13:24:13 +09:00
|
|
|
className="bg-card rounded-xl border border-border p-6 shadow-[var(--cp-shadow-1)]"
|
2025-10-22 10:58:16 +09:00
|
|
|
>
|
2025-12-24 13:24:13 +09:00
|
|
|
<div className="flex items-start gap-4">
|
|
|
|
|
<Skeleton className="h-12 w-12 rounded-xl" />
|
|
|
|
|
<div className="flex-1 space-y-2">
|
|
|
|
|
<Skeleton className="h-6 w-48" />
|
|
|
|
|
<Skeleton className="h-4 w-72" />
|
|
|
|
|
<Skeleton className="h-6 w-32" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-22 10:58:16 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</AsyncBlock>
|
2025-12-25 11:54:33 +09:00
|
|
|
</div>
|
2025-12-16 13:54:31 +09:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 19:01:21 +09:00
|
|
|
// Determine current status for the badge
|
|
|
|
|
const currentStatus = isEligible
|
|
|
|
|
? "eligible"
|
|
|
|
|
: isPending
|
|
|
|
|
? "pending"
|
|
|
|
|
: isIneligible
|
|
|
|
|
? "ineligible"
|
|
|
|
|
: "not_requested";
|
|
|
|
|
|
|
|
|
|
// Case 1: Unverified / Not Requested - Show Public Content exactly
|
|
|
|
|
if (isNotRequested && autoRequestStatus !== "submitting" && autoRequestStatus !== "submitted") {
|
|
|
|
|
return (
|
2025-12-25 11:54:33 +09:00
|
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20 pt-8">
|
2025-12-24 19:01:21 +09:00
|
|
|
{/* Already has internet warning */}
|
|
|
|
|
{hasActiveInternet && (
|
|
|
|
|
<AlertBanner variant="warning" title="Active subscription found" className="mb-6">
|
|
|
|
|
You already have an internet subscription. For additional residences, please{" "}
|
|
|
|
|
<a href="/account/support/new" className="underline font-medium">
|
|
|
|
|
contact support
|
|
|
|
|
</a>
|
|
|
|
|
.
|
2025-12-22 18:59:38 +09:00
|
|
|
</AlertBanner>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-12-24 19:01:21 +09:00
|
|
|
{/* Auto-request status alerts - only show for errors/success */}
|
2025-12-22 18:59:38 +09:00
|
|
|
{autoRequestStatus === "failed" && (
|
2025-12-24 19:01:21 +09:00
|
|
|
<AlertBanner variant="warning" title="Request failed" className="mb-6">
|
|
|
|
|
Please try again below or contact support.
|
2025-12-22 18:59:38 +09:00
|
|
|
</AlertBanner>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{autoRequestStatus === "missing_address" && (
|
2025-12-24 19:01:21 +09:00
|
|
|
<AlertBanner variant="warning" title="Address required" className="mb-6">
|
|
|
|
|
<div className="flex items-center justify-between gap-4">
|
|
|
|
|
<span>Add your service address to request availability verification.</span>
|
|
|
|
|
<Button as="a" href="/account/settings" size="sm">
|
|
|
|
|
Add address
|
2025-12-19 15:15:36 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</AlertBanner>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
<PublicInternetPlansContent
|
|
|
|
|
onCtaClick={handleCheckAvailability}
|
|
|
|
|
ctaLabel={hasServiceAddress ? "Check Availability" : "Add Service Address"}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-24 13:24:13 +09:00
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
// Case 2: Standard Portal View (Pending, Eligible, Ineligible, Loading)
|
|
|
|
|
return (
|
|
|
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-20 pt-8">
|
|
|
|
|
<CatalogBackLink href={servicesBasePath} label="Back to Services" className="mb-6" />
|
|
|
|
|
|
|
|
|
|
{/* Hero section - compact (for portal view) */}
|
|
|
|
|
<div className="text-center mb-8">
|
|
|
|
|
<h1 className="text-2xl md:text-3xl font-bold text-foreground mb-2">
|
|
|
|
|
Your Internet Options
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-muted-foreground mb-4">
|
|
|
|
|
Plans tailored to your residence and available infrastructure
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{/* Status badge */}
|
|
|
|
|
{!eligibilityLoading && autoRequestStatus !== "submitting" && (
|
|
|
|
|
<EligibilityStatusBadge status={currentStatus} speed={eligibilityDisplay?.speed} />
|
|
|
|
|
)}
|
2025-12-18 18:12:20 +09:00
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
{/* Loading states */}
|
|
|
|
|
{(eligibilityLoading || autoRequestStatus === "submitting") && (
|
|
|
|
|
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-border bg-muted">
|
|
|
|
|
<div className="h-4 w-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
|
|
|
|
<span className="font-medium text-sm text-foreground">
|
|
|
|
|
{autoRequestStatus === "submitting" ? "Submitting request..." : "Checking status..."}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Already has internet warning */}
|
|
|
|
|
{hasActiveInternet && (
|
|
|
|
|
<AlertBanner variant="warning" title="Active subscription found" className="mb-6">
|
|
|
|
|
You already have an internet subscription. For additional residences, please{" "}
|
|
|
|
|
<a href="/account/support/new" className="underline font-medium">
|
|
|
|
|
contact support
|
|
|
|
|
</a>
|
|
|
|
|
.
|
|
|
|
|
</AlertBanner>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Auto-request status alerts - only show for errors/success */}
|
|
|
|
|
{autoRequestStatus === "submitted" && (
|
|
|
|
|
<AlertBanner variant="success" title="Request submitted" className="mb-6">
|
|
|
|
|
We'll verify your address and notify you when complete.
|
|
|
|
|
{autoRequestId && (
|
|
|
|
|
<span className="text-xs text-muted-foreground ml-2">ID: {autoRequestId}</span>
|
|
|
|
|
)}
|
|
|
|
|
</AlertBanner>
|
|
|
|
|
)}
|
2025-10-22 10:58:16 +09:00
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
{autoRequestStatus === "failed" && (
|
|
|
|
|
<AlertBanner variant="warning" title="Request failed" className="mb-6">
|
|
|
|
|
Please try again below or contact support.
|
|
|
|
|
</AlertBanner>
|
|
|
|
|
)}
|
2025-12-24 13:24:13 +09:00
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
{autoRequestStatus === "missing_address" && (
|
|
|
|
|
<AlertBanner variant="warning" title="Address required" className="mb-6">
|
|
|
|
|
<div className="flex items-center justify-between gap-4">
|
|
|
|
|
<span>Add your service address to request availability verification.</span>
|
|
|
|
|
<Button as="a" href="/account/settings" size="sm">
|
|
|
|
|
Add address
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</AlertBanner>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ELIGIBLE STATE - Clean & Personalized */}
|
|
|
|
|
{isEligible && eligibilityDisplay && offeringCards.length > 0 && (
|
|
|
|
|
<>
|
|
|
|
|
{/* Plan comparison guide */}
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<PlanComparisonGuide />
|
|
|
|
|
</div>
|
2025-12-24 13:24:13 +09:00
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
{/* Speed options header (only if multiple) */}
|
|
|
|
|
{offeringCards.length > 1 && (
|
|
|
|
|
<div className="mb-4">
|
|
|
|
|
<h2 className="text-lg font-semibold text-foreground">Choose your speed</h2>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Your address supports multiple options
|
2025-12-24 13:24:13 +09:00
|
|
|
</p>
|
2025-12-25 11:54:33 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-24 13:24:13 +09:00
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
{/* Offering cards */}
|
|
|
|
|
<div className="space-y-4 mb-6">
|
|
|
|
|
{offeringCards.map(card => (
|
|
|
|
|
<div key={card.offeringType}>
|
|
|
|
|
{card.isAlternative && (
|
|
|
|
|
<div className="flex items-center gap-2 mb-3">
|
|
|
|
|
<div className="h-px flex-1 bg-border" />
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
|
|
|
Alternative option
|
|
|
|
|
</span>
|
|
|
|
|
<div className="h-px flex-1 bg-border" />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<InternetOfferingCard
|
|
|
|
|
offeringType={card.offeringType}
|
|
|
|
|
title={card.title}
|
|
|
|
|
speedBadge={card.speedBadge}
|
|
|
|
|
description={card.alternativeNote ?? card.description}
|
|
|
|
|
iconType={card.iconType}
|
|
|
|
|
startingPrice={card.startingPrice}
|
|
|
|
|
setupFee={card.setupFee}
|
|
|
|
|
tiers={card.tiers}
|
|
|
|
|
ctaPath={card.ctaPath}
|
|
|
|
|
isPremium={card.isPremium}
|
|
|
|
|
defaultExpanded={false}
|
|
|
|
|
disabled={hasActiveInternet}
|
|
|
|
|
disabledReason={
|
|
|
|
|
hasActiveInternet ? "Contact support for additional lines" : undefined
|
|
|
|
|
}
|
|
|
|
|
/>
|
2025-12-24 13:24:13 +09:00
|
|
|
</div>
|
2025-12-25 11:54:33 +09:00
|
|
|
))}
|
|
|
|
|
</div>
|
2025-12-24 19:01:21 +09:00
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
{/* Important notes - collapsed by default */}
|
|
|
|
|
<InternetImportantNotes />
|
2025-12-24 13:24:13 +09:00
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
<CatalogBackLink
|
|
|
|
|
href={servicesBasePath}
|
|
|
|
|
label="Back to Services"
|
|
|
|
|
align="center"
|
|
|
|
|
className="mt-10"
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2025-12-24 13:24:13 +09:00
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
{/* PENDING STATE - Clean Status View */}
|
|
|
|
|
{isPending && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="bg-info-soft/30 border border-info/20 rounded-xl p-8 mb-6 text-center max-w-2xl mx-auto">
|
|
|
|
|
<Clock className="h-16 w-16 text-info mx-auto mb-6" />
|
|
|
|
|
<h2 className="text-2xl font-semibold text-foreground mb-3">
|
|
|
|
|
Verification in Progress
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="text-base text-muted-foreground mb-4 leading-relaxed">
|
|
|
|
|
We're currently verifying NTT service availability at your registered address.
|
|
|
|
|
<br />
|
|
|
|
|
This manual check ensures we offer you the correct fiber connection type.
|
2025-12-24 19:01:21 +09:00
|
|
|
</p>
|
2025-12-25 11:54:33 +09:00
|
|
|
|
|
|
|
|
<div className="inline-flex flex-col items-center p-4 bg-background rounded-lg border border-border">
|
|
|
|
|
<span className="text-sm font-medium text-foreground mb-1">Estimated time</span>
|
|
|
|
|
<span className="text-sm text-muted-foreground">1-2 business days</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{requestedAt && (
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-6">
|
|
|
|
|
Request submitted: {new Date(requestedAt).toLocaleDateString()}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<Button as="a" href={servicesBasePath} variant="outline">
|
|
|
|
|
Back to Catalog
|
2025-12-24 19:01:21 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-12-25 11:54:33 +09:00
|
|
|
</>
|
|
|
|
|
)}
|
2025-12-24 19:01:21 +09:00
|
|
|
|
2025-12-25 11:54:33 +09:00
|
|
|
{/* INELIGIBLE STATE */}
|
|
|
|
|
{isIneligible && (
|
|
|
|
|
<div className="bg-warning/5 border border-warning/20 rounded-xl p-6 text-center">
|
|
|
|
|
<TriangleAlert className="h-12 w-12 text-warning mx-auto mb-4" />
|
|
|
|
|
<h2 className="text-lg font-semibold text-foreground mb-2">Service not available</h2>
|
|
|
|
|
<p className="text-sm text-muted-foreground mb-4 max-w-md mx-auto">
|
|
|
|
|
{rejectionNotes ||
|
|
|
|
|
"Our review determined that NTT fiber service isn't available at your address."}
|
|
|
|
|
</p>
|
|
|
|
|
<Button as="a" href="/account/support/new" variant="outline">
|
|
|
|
|
Contact support
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* No plans available */}
|
|
|
|
|
{plans.length === 0 && !isLoading && (
|
|
|
|
|
<div className="text-center py-16">
|
|
|
|
|
<div className="bg-card rounded-2xl shadow-[var(--cp-shadow-1)] border border-border p-12 max-w-md mx-auto">
|
|
|
|
|
<Server className="h-16 w-16 text-muted-foreground mx-auto mb-6" />
|
|
|
|
|
<h3 className="text-xl font-semibold text-foreground mb-2">No Plans Available</h3>
|
|
|
|
|
<p className="text-muted-foreground mb-8">
|
|
|
|
|
We couldn't find any internet plans at this time.
|
|
|
|
|
</p>
|
|
|
|
|
<CatalogBackLink href={servicesBasePath} label="Back to Services" align="center" />
|
2025-12-16 13:54:31 +09:00
|
|
|
</div>
|
2025-12-25 11:54:33 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-09-17 18:43:43 +09:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 17:59:55 +09:00
|
|
|
export default InternetPlansContainer;
|