Refactor authentication and user profile management to improve type consistency and error handling. Update type definitions for requests and responses across various components, ensuring alignment with shared domain contracts. Enhance state management in AppShell and streamline user data hydration logic for better maintainability.

This commit is contained in:
barsa 2025-09-25 18:04:47 +09:00
parent ec69e3dcbb
commit b6d0aa1eb0
20 changed files with 248 additions and 150 deletions

View File

@ -75,17 +75,19 @@ export function AppShell({ children }: AppShellProps) {
if (!prof) {
return;
}
useAuthStore.setState(state => ({
...state,
user: state.user
useAuthStore.setState(state =>
state.user
? {
...state,
user: {
...state.user,
firstName: prof.firstName || state.user.firstName,
lastName: prof.lastName || state.user.lastName,
phone: prof.phone || state.user.phone,
},
}
: prof,
}));
: state
);
} catch {
// best-effort profile hydration; ignore errors
}

View File

@ -7,7 +7,7 @@ import { useWhmcsLink } from "@/features/auth/hooks";
import {
linkWhmcsRequestSchema,
type LinkWhmcsFormData,
type LinkWhmcsRequestData,
type LinkWhmcsRequestInput,
} from "@customer-portal/domain";
import { useZodForm } from "@customer-portal/validation";
@ -23,7 +23,7 @@ export function LinkWhmcsForm({ onTransferred, className = "" }: LinkWhmcsFormPr
async (formData: LinkWhmcsFormData) => {
clearError();
try {
const payload: LinkWhmcsRequestData = {
const payload: LinkWhmcsRequestInput = {
email: formData.email,
password: formData.password,
};

View File

@ -9,7 +9,7 @@ import { useState, useCallback, useMemo } from "react";
import Link from "next/link";
import { ErrorMessage } from "@/components/atoms";
import { useSignup } from "../../hooks/use-auth";
import { signupFormSchema, signupFormToRequest, type SignupRequest } from "@customer-portal/domain";
import { signupFormSchema, signupFormToRequest, type SignupRequestInput } from "@customer-portal/domain";
import { useZodForm } from "@customer-portal/validation";
import { z } from "zod";
@ -57,18 +57,11 @@ export function SignupForm({
const handleSignup = useCallback(
async ({
confirmPassword: _confirm,
acceptTerms,
marketingConsent,
...formData
}: SignupFormValues) => {
clearError();
try {
const baseRequest = signupFormToRequest(formData);
const request: SignupRequest = {
...baseRequest,
acceptTerms,
marketingConsent,
};
const request: SignupRequestInput = signupFormToRequest(formData);
await signup(request);
onSuccess?.();
} catch (err) {

View File

@ -9,7 +9,7 @@ import { useCallback, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useAuthStore } from "../services/auth.store";
import { getPostLoginRedirect } from "@/features/auth/utils/route-protection";
import type { SignupRequest, LoginRequest } from "@customer-portal/domain";
import type { SignupRequestInput, LoginRequestInput } from "@customer-portal/domain";
/**
* Main authentication hook
@ -21,7 +21,7 @@ export function useAuth() {
// Enhanced login with redirect handling
const login = useCallback(
async (credentials: LoginRequest) => {
async (credentials: LoginRequestInput) => {
await store.login(credentials);
const redirectTo = getPostLoginRedirect(searchParams);
router.push(redirectTo);
@ -31,7 +31,7 @@ export function useAuth() {
// Enhanced signup with redirect handling
const signup = useCallback(
async (data: SignupRequest) => {
async (data: SignupRequestInput) => {
await store.signup(data);
const redirectTo = getPostLoginRedirect(searchParams);
router.push(redirectTo);

View File

@ -10,10 +10,10 @@ import { getErrorInfo, handleAuthError } from "@/lib/utils/error-handling";
import logger from "@customer-portal/logging";
import type {
AuthTokens,
UserProfile,
LinkWhmcsRequestData,
LoginRequest,
SignupRequest,
AuthenticatedUser,
LinkWhmcsRequestInput,
LoginRequestInput,
SignupRequestInput,
} from "@customer-portal/domain";
import { authResponseSchema } from "@customer-portal/domain/validation";
@ -28,7 +28,7 @@ const withAuthHeaders = (accessToken?: string) =>
interface AuthState {
// State
user: UserProfile | null;
user: AuthenticatedUser | null;
tokens: AuthTokens | null;
isAuthenticated: boolean;
loading: boolean;
@ -37,15 +37,15 @@ interface AuthState {
hasCheckedAuth: boolean;
// Actions
login: (credentials: LoginRequest) => Promise<void>;
signup: (data: SignupRequest) => Promise<void>;
login: (credentials: LoginRequestInput) => Promise<void>;
signup: (data: SignupRequestInput) => Promise<void>;
logout: () => Promise<void>;
requestPasswordReset: (email: string) => Promise<void>;
resetPassword: (token: string, password: string) => Promise<void>;
changePassword: (currentPassword: string, newPassword: string) => Promise<void>;
checkPasswordNeeded: (email: string) => Promise<{ needsPasswordSet: boolean }>;
linkWhmcs: (
request: LinkWhmcsRequestData
request: LinkWhmcsRequestInput
) => Promise<{ needsPasswordSet: boolean; email: string }>;
setPassword: (email: string, password: string) => Promise<void>;
refreshUser: () => Promise<void>;
@ -70,7 +70,7 @@ export const useAuthStore = create<AuthState>()(
hasCheckedAuth: false,
// Actions
login: async (credentials: LoginRequest) => {
login: async (credentials: LoginRequestInput) => {
set({ loading: true, error: null });
try {
// Use shared API client with consistent configuration
@ -99,7 +99,7 @@ export const useAuthStore = create<AuthState>()(
}
},
signup: async (data: SignupRequest) => {
signup: async (data: SignupRequestInput) => {
set({ loading: true, error: null });
try {
const response = await apiClient.POST("/auth/signup", { body: data });
@ -248,7 +248,7 @@ export const useAuthStore = create<AuthState>()(
}
},
linkWhmcs: async ({ email, password }: LinkWhmcsRequestData) => {
linkWhmcs: async ({ email, password }: LinkWhmcsRequestInput) => {
set({ loading: true, error: null });
try {
const response = await apiClient.POST("/auth/link-whmcs", {
@ -305,11 +305,11 @@ export const useAuthStore = create<AuthState>()(
if (!tokens?.accessToken) return;
try {
const response = await apiClient.GET<UserProfile>("/me", {
const response = await apiClient.GET<AuthenticatedUser>("/me", {
...withAuthHeaders(tokens.accessToken),
});
const profile = getNullableData<UserProfile>(response);
const profile = getNullableData<AuthenticatedUser>(response);
if (!profile) {
// Token might be expired, try to refresh
await get().refreshTokens();

View File

@ -11,14 +11,16 @@ import { AnimatedCard } from "@/components/molecules";
import { Button } from "@/components/atoms/button";
import { useRouter } from "next/navigation";
// Use consolidated domain types
import type { OrderItemRequest, OrderTotals as DomainOrderTotals } from "@customer-portal/domain";
// Align with shared catalog contracts
import type { CatalogProductBase } from "@customer-portal/domain";
// Enhanced OrderItem for UI - properly extends unified types instead of redefining everything
export interface OrderItem extends Omit<OrderItemRequest, "id"> {
id?: string; // Optional for UI purposes (OrderItemRequest.id is required)
description?: string;
}
// Enhanced order item representation for UI summary
export type OrderItem = CatalogProductBase & {
id?: string;
quantity?: number;
autoAdded?: boolean;
itemClass?: string;
};
export interface OrderConfiguration {
label: string;
@ -26,8 +28,10 @@ export interface OrderConfiguration {
important?: boolean;
}
// Extend domain OrderTotals with UI-specific fields
export interface OrderTotals extends DomainOrderTotals {
// Totals summary for UI; base fields mirror API aggregates
export interface OrderTotals {
monthlyTotal: number;
oneTimeTotal: number;
annualTotal?: number;
discountAmount?: number;
taxAmount?: number;

View File

@ -1,5 +1,5 @@
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
import type { CatalogOrderItem } from "@customer-portal/domain";
import type { CatalogProductBase } from "@customer-portal/domain";
import { useRouter } from "next/navigation";
import { getMonthlyPrice, getOneTimePrice } from "../../utils/pricing";
@ -11,8 +11,8 @@ interface OrderSummaryProps {
};
// Selected items
selectedAddons?: CatalogOrderItem[];
activationFees?: CatalogOrderItem[];
selectedAddons?: CatalogProductBase[];
activationFees?: CatalogProductBase[];
// Configuration details
configDetails?: Array<{

View File

@ -14,7 +14,7 @@ import { InstallationStep } from "./steps/InstallationStep";
import { AddonsStep } from "./steps/AddonsStep";
import { ReviewOrderStep } from "./steps/ReviewOrderStep";
import { useConfigureState } from "./hooks/useConfigureState";
import { getMonthlyPrice, getOneTimePrice } from "../../utils/pricing";
import { getMonthlyPrice, getOneTimePrice } from "../../../utils/pricing";
interface Props {
plan: InternetPlanCatalogItem | null;
@ -29,7 +29,7 @@ const STEPS = [
{ number: 2, title: "Installation", description: "Installation method" },
{ number: 3, title: "Add-ons", description: "Optional services" },
{ number: 4, title: "Review", description: "Order summary" },
];
] as const;
export function InternetConfigureContainer({
plan,
@ -56,6 +56,11 @@ export function InternetConfigureContainer({
const handleAddonSelection = (newSelectedSkus: string[]) => setSelectedAddonSkus(newSelectedSkus);
const progressSteps = STEPS.map(step => ({
...step,
completed: currentStep > step.number,
}));
if (loading) {
return <ConfigureLoadingSkeleton />;
}
@ -86,7 +91,7 @@ export function InternetConfigureContainer({
{/* Progress Steps */}
<div className="mb-8">
<ProgressSteps steps={STEPS} currentStep={currentStep} />
<ProgressSteps steps={progressSteps} currentStep={currentStep} />
</div>
{/* Step Content */}
@ -106,7 +111,6 @@ export function InternetConfigureContainer({
installations={installations}
selectedInstallation={selectedInstallation}
setSelectedInstallationSku={setSelectedInstallationSku}
selectedInstallationType={selectedInstallationType}
isTransitioning={isTransitioning}
onBack={() => transitionToStep(1)}
onNext={() => canProceedFromStep(2) && transitionToStep(3)}

View File

@ -11,7 +11,6 @@ interface Props {
installations: InternetInstallationCatalogItem[];
selectedInstallation: InternetInstallationCatalogItem | null;
setSelectedInstallationSku: (sku: string | null) => void;
selectedInstallationType: string | null;
isTransitioning: boolean;
onBack: () => void;
onNext: () => void;
@ -21,7 +20,6 @@ export function InstallationStep({
installations,
selectedInstallation,
setSelectedInstallationSku,
selectedInstallationType,
isTransitioning,
onBack,
onNext,
@ -43,9 +41,10 @@ export function InstallationStep({
<InstallationOptions
installations={installations}
selectedInstallation={selectedInstallation}
setSelectedInstallationSku={setSelectedInstallationSku}
selectedInstallationType={selectedInstallationType}
selectedInstallationSku={selectedInstallation?.sku ?? null}
onInstallationSelect={installation =>
setSelectedInstallationSku(installation ? installation.sku : null)
}
/>
<div className="flex justify-between mt-6">

View File

@ -92,10 +92,10 @@ function SilverPlanConfiguration({
details="Traditional connection method with username/password authentication. Compatible with most routers and ISPs."
/>
<ModeSelectionCard
mode="IPoE"
mode="IPoE-BYOR"
selectedMode={mode}
onSelect={setMode}
title="IPoE"
title="IPoE (BYOR)"
description="IP over Ethernet"
details="Modern connection method with automatic configuration. Simplified setup with faster connection times."
/>

View File

@ -3,10 +3,10 @@
import { AnimatedCard } from "@/components/molecules";
import { Button } from "@/components/atoms/button";
import { CurrencyYenIcon } from "@heroicons/react/24/outline";
import type { VpnPlan } from "@customer-portal/domain";
import type { VpnCatalogProduct } from "@customer-portal/domain";
interface VpnPlanCardProps {
plan: VpnPlan;
plan: VpnCatalogProduct;
}
export function VpnPlanCard({ plan }: VpnPlanCardProps) {

View File

@ -118,7 +118,7 @@ export function SimPlansContainer() {
);
}
const plansByType: PlansByType = simPlans.reduce(
const plansByType = simPlans.reduce<PlansByType>(
(acc, plan) => {
const planType = plan.simPlanType || "DataOnly";
if (planType === "DataOnly") acc.DataOnly.push(plan);

View File

@ -6,32 +6,34 @@ import { catalogService } from "@/features/catalog/services/catalog.service";
import { ordersService } from "@/features/orders/services/orders.service";
import { usePaymentMethods } from "@/features/billing/hooks/useBilling";
import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh";
import type {
InternetPlanCatalogItem,
InternetInstallationCatalogItem,
InternetAddonCatalogItem,
SimCatalogProduct,
SimActivationFeeCatalogItem,
VpnCatalogProduct,
} from "@customer-portal/domain";
import {
buildInternetOrderItems,
buildSimOrderItems,
calculateOrderTotals,
extractOrderSKUs,
createLoadingState,
createSuccessState,
createErrorState,
} from "@customer-portal/domain";
import type { AsyncState, CheckoutCart, CatalogOrderItem } from "@customer-portal/domain";
// Type alias for convenience
type OrderItem = CatalogOrderItem;
import { getMonthlyPrice, getOneTimePrice } from "@/features/catalog/utils/pricing";
import type { CatalogProductBase } from "@customer-portal/domain";
import { createLoadingState, createSuccessState, createErrorState } from "@customer-portal/domain";
import type { AsyncState } from "@customer-portal/domain";
import { useActiveSubscriptions } from "@/features/subscriptions/hooks/useSubscriptions";
// Use domain Address type
import type { Address } from "@customer-portal/domain";
type CheckoutItemType = "plan" | "installation" | "addon" | "activation" | "vpn";
interface CheckoutItem extends CatalogProductBase {
quantity: number;
itemType: CheckoutItemType;
autoAdded?: boolean;
}
interface CheckoutTotals {
monthlyTotal: number;
oneTimeTotal: number;
}
interface CheckoutCart {
items: CheckoutItem[];
totals: CheckoutTotals;
configuration: Record<string, unknown>;
}
export function useCheckout() {
const params = useSearchParams();
const router = useRouter();
@ -84,68 +86,142 @@ export function useCheckout() {
useEffect(() => {
let mounted = true;
const collectAddonRefs = () => {
const refs = new Set<string>();
params.getAll("addonSku").forEach(sku => {
if (sku) refs.add(sku);
});
if (selections.addonSku) refs.add(selections.addonSku);
if (selections.addons) {
selections.addons
.split(",")
.map(value => value.trim())
.filter(Boolean)
.forEach(value => refs.add(value));
}
return Array.from(refs);
};
const calculateTotals = (items: CheckoutItem[]): CheckoutTotals =>
items.reduce<CheckoutTotals>(
(acc, item) => {
acc.monthlyTotal += getMonthlyPrice(item) * item.quantity;
acc.oneTimeTotal += getOneTimePrice(item) * item.quantity;
return acc;
},
{ monthlyTotal: 0, oneTimeTotal: 0 }
);
void (async () => {
try {
setCheckoutState(createLoadingState());
if (!selections.plan) {
const planRef =
selections.plan ??
selections.planId ??
selections.planSku ??
selections.planIdSku ??
null;
if (!planRef) {
throw new Error("No plan selected. Please go back and select a plan.");
}
let items: OrderItem[] = [];
const addonRefs = collectAddonRefs();
const items: CheckoutItem[] = [];
if (orderType === "Internet") {
const [plans, addons, installations] = await Promise.all([
catalogService.getInternetPlans(),
catalogService.getInternetAddons(),
catalogService.getInternetInstallations(),
]);
const { plans, addons, installations } = await catalogService.getInternetCatalog();
const plan = plans.find(p => p.sku === selections.plan);
const plan = plans.find(p => p.sku === planRef || p.id === planRef) ?? null;
if (!plan) {
throw new Error(
`Internet plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`
`Internet plan not found for reference: ${planRef}. Please go back and select a valid plan.`
);
}
const addonSkus: string[] = [];
const urlParams = new URLSearchParams(window.location.search);
urlParams.getAll("addonSku").forEach(sku => {
if (sku && !addonSkus.includes(sku)) addonSkus.push(sku);
});
items.push({ ...plan, quantity: 1, itemType: "plan" });
items = buildInternetOrderItems(plan, addons, installations, {
installationSku: selections.installationSku,
addonSkus: addonSkus.length > 0 ? addonSkus : undefined,
if (selections.installationSku) {
const installation =
installations.find(
inst =>
inst.sku === selections.installationSku || inst.id === selections.installationSku
) ?? null;
if (!installation) {
throw new Error(
`Installation option not found for reference: ${selections.installationSku}. Please reselect your installation method.`
);
}
items.push({ ...installation, quantity: 1, itemType: "installation" });
}
addonRefs.forEach(ref => {
const addon = addons.find(a => a.sku === ref || a.id === ref) ?? null;
if (addon) {
items.push({ ...addon, quantity: 1, itemType: "addon" });
}
});
} else if (orderType === "SIM") {
const [plans, activationFees, addons] = await Promise.all([
catalogService.getSimPlans(),
catalogService.getSimActivationFees(),
catalogService.getSimAddons(),
]);
const { plans, activationFees, addons } = await catalogService.getSimCatalog();
const plan = plans.find(p => p.sku === selections.plan);
const plan = plans.find(p => p.sku === planRef || p.id === planRef) ?? null;
if (!plan) {
throw new Error(
`SIM plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`
`SIM plan not found for reference: ${planRef}. Please go back and select a valid plan.`
);
}
const addonSkus: string[] = [];
if (selections.addonSku) addonSkus.push(selections.addonSku);
const urlParams = new URLSearchParams(window.location.search);
urlParams.getAll("addonSku").forEach(sku => {
if (sku && !addonSkus.includes(sku)) addonSkus.push(sku);
items.push({ ...plan, quantity: 1, itemType: "plan" });
addonRefs.forEach(ref => {
const addon = addons.find(a => a.sku === ref || a.id === ref) ?? null;
if (addon) {
items.push({ ...addon, quantity: 1, itemType: "addon" });
}
});
items = buildSimOrderItems(plan, activationFees, addons, {
addonSkus: addonSkus.length > 0 ? addonSkus : undefined,
});
const simType = selections.simType ?? "eSIM";
const activation =
activationFees.find(fee => {
const feeSimType =
(fee as unknown as { simType?: string }).simType ||
((fee.catalogMetadata as { simType?: string } | undefined)?.simType ?? undefined);
return feeSimType
? feeSimType === simType
: fee.sku === selections.activationFeeSku || fee.id === selections.activationFeeSku;
}) ?? null;
if (activation) {
items.push({ ...activation, quantity: 1, itemType: "activation" });
}
} else if (orderType === "VPN") {
const { plans, activationFees } = await catalogService.getVpnCatalog();
const plan = plans.find(p => p.sku === planRef || p.id === planRef) ?? null;
if (!plan) {
throw new Error(
`VPN plan not found for reference: ${planRef}. Please go back and select a valid plan.`
);
}
if (mounted) {
const totals = calculateOrderTotals(items);
items.push({ ...plan, quantity: 1, itemType: "vpn" });
const activation =
activationFees.find(
fee => fee.sku === selections.activationSku || fee.id === selections.activationSku
) ?? null;
if (activation) {
items.push({ ...activation, quantity: 1, itemType: "activation" });
}
} else {
throw new Error("Unsupported order type. Please begin checkout from the catalog.");
}
if (!mounted) return;
const totals = calculateTotals(items);
setCheckoutState(
createSuccessState({
items,
@ -153,7 +229,6 @@ export function useCheckout() {
configuration: {},
})
);
}
} catch (error) {
if (mounted) {
setCheckoutState(
@ -164,10 +239,11 @@ export function useCheckout() {
}
}
})();
return () => {
mounted = false;
};
}, [orderType, selections]);
}, [orderType, params, selections]);
const handleSubmitOrder = useCallback(async () => {
try {
@ -175,8 +251,14 @@ export function useCheckout() {
if (checkoutState.status !== "success") {
throw new Error("Checkout data not loaded");
}
const skus = extractOrderSKUs(checkoutState.data.items);
if (!skus || skus.length === 0) {
const uniqueSkus = Array.from(
new Set(
checkoutState.data.items
.map(item => item.sku)
.filter((sku): sku is string => typeof sku === "string" && sku.trim().length > 0)
)
);
if (uniqueSkus.length === 0) {
throw new Error("No products selected for order. Please go back and select products.");
}
@ -207,7 +289,7 @@ export function useCheckout() {
const orderData = {
orderType,
skus,
skus: uniqueSkus,
...(Object.keys(configurations).length > 0 && { configurations }),
};

View File

@ -63,7 +63,7 @@ export function OrdersListContainer() {
useEffect(() => {
const fetchOrders = async () => {
try {
const list = await ordersService.getMyOrders<OrderSummary>();
const list = await ordersService.getMyOrders<OrderSummary[]>();
setOrders(list);
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to load orders");

View File

@ -30,7 +30,7 @@ function InfoRow({ label, value }: { label: string; value: string }) {
export function SimCancelContainer() {
const params = useParams();
const router = useRouter();
const subscriptionId = parseInt(params.id as string);
const subscriptionId = params.id as string;
const [step, setStep] = useState<Step>(1);
const [loading, setLoading] = useState(false);
@ -94,12 +94,17 @@ export function SimCancelContainer() {
const emailsMatch = !emailProvided || email.trim() === email2.trim();
const canProceedStep3 =
acceptTerms && !!cancelMonth && confirmMonthEnd && emailValid && emailsMatch;
const runDate = cancelMonth ? `${cancelMonth}01` : undefined;
const runDate = cancelMonth ? `${cancelMonth}01` : null;
const submit = async () => {
setLoading(true);
setError(null);
setMessage(null);
if (!runDate) {
setError("Please select a cancellation month before submitting.");
setLoading(false);
return;
}
try {
await simActionsService.cancel(subscriptionId, { scheduledAt: runDate });
setMessage("Cancellation request submitted. You will receive a confirmation email.");

View File

@ -7,6 +7,7 @@ export interface TopUpRequest {
export interface ChangePlanRequest {
newPlanCode: string;
assignGlobalIp: boolean;
scheduledAt?: string;
}
export interface CancelRequest {
@ -41,9 +42,12 @@ export const simActionsService = {
},
async getSimInfo<T, E = unknown>(subscriptionId: string): Promise<SimInfo<T, E> | null> {
const response = await apiClient.GET("/api/subscriptions/{subscriptionId}/sim/info", {
const response = await apiClient.GET<SimInfo<T, E> | null>(
"/api/subscriptions/{subscriptionId}/sim/info",
{
params: { path: { subscriptionId } },
});
return getDataOrDefault(response, null);
}
);
return getDataOrDefault<SimInfo<T, E> | null>(response, null);
},
};

View File

@ -30,7 +30,7 @@ function InfoRow({ label, value }: { label: string; value: string }) {
export function SimCancelContainer() {
const params = useParams();
const router = useRouter();
const subscriptionId = parseInt(params.id as string);
const subscriptionId = params.id as string;
const [step, setStep] = useState<Step>(1);
const [loading, setLoading] = useState(false);
@ -92,12 +92,17 @@ export function SimCancelContainer() {
const emailsMatch = !emailProvided || email.trim() === email2.trim();
const canProceedStep3 =
acceptTerms && !!cancelMonth && confirmMonthEnd && emailValid && emailsMatch;
const runDate = cancelMonth ? `${cancelMonth}01` : undefined;
const runDate = cancelMonth ? `${cancelMonth}01` : null;
const submit = async () => {
setLoading(true);
setError(null);
setMessage(null);
if (!runDate) {
setError("Please select a cancellation month before submitting.");
setLoading(false);
return;
}
try {
await simActionsService.cancel(subscriptionId, { scheduledAt: runDate });
setMessage("Cancellation request submitted. You will receive a confirmation email.");

View File

@ -20,7 +20,7 @@ const PLAN_LABELS: Record<PlanCode, string> = {
export function SimChangePlanContainer() {
const params = useParams();
const subscriptionId = parseInt(params.id as string);
const subscriptionId = params.id as string;
const [currentPlanCode] = useState<string>("");
const [newPlanCode, setNewPlanCode] = useState<"" | PlanCode>("");
const [assignGlobalIp, setAssignGlobalIp] = useState(false);

View File

@ -11,7 +11,7 @@ import { DevicePhoneMobileIcon } from "@heroicons/react/24/outline";
export function SimTopUpContainer() {
const params = useParams();
const subscriptionId = parseInt(params.id as string);
const subscriptionId = params.id as string;
const [gbAmount, setGbAmount] = useState<string>("1");
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);

View File

@ -211,14 +211,14 @@ function deriveInfoFromPayload(payload: unknown, status?: number): ApiErrorInfo
"message" in payload &&
typeof (payload as { message?: unknown }).message === "string"
) {
const payloadWithMessage = payload as { code?: unknown; message: string };
const candidateCode = payloadWithMessage.code;
const code =
typeof (payload as { code?: unknown }).code === "string"
? (payload as { code: string }).code
: httpStatusCodeToLabel(status);
typeof candidateCode === "string" ? candidateCode : httpStatusCodeToLabel(status);
return {
code,
message: (payload as { message: string }).message,
message: payloadWithMessage.message,
shouldLogout: shouldLogoutForError(code) || status === 401,
shouldRetry: typeof status === "number" ? status >= 500 : shouldRetryForError(code),
};