feat: show SMS channel registration status (#78)

* feat: show SMS channel registration status

* fix: report VoWiFi status refresh failures

* fix: ignore stale VoWiFi refresh completion

* fix: invalidate refreshes on VoWiFi mode changes

* fix: align SMS status with active network path

---------

Co-authored-by: geekouc <[email protected]>
This commit is contained in:
jirong0214
2026-08-22 14:01:38 +08:00
committed by GitHub
co-authored by geekouc
parent 4a39ed3d31
commit cb8152ccd5
4 changed files with 130 additions and 3 deletions
@@ -19,7 +19,7 @@ export interface DeviceOverviewTabProps {
trafficMinuteTx: string;
e911Starting: boolean;
onSetupE911: () => void;
onRefresh: () => void;
onRefresh: () => void | Promise<void>;
}
export function DeviceOverviewTab(props: DeviceOverviewTabProps) {
@@ -45,6 +45,7 @@ export function DeviceOverviewTab(props: DeviceOverviewTabProps) {
customPhoneNumber={props.customPhoneNumber}
e911Starting={props.e911Starting}
onSetupE911={props.onSetupE911}
onRefreshOverview={props.onRefresh}
/>
{showNetworkDetails ? <OverviewNetworkPanel
device={device}
@@ -6,6 +6,7 @@ import type { DeviceDetail } from "./types";
import { useI18n } from "../../lib/i18n";
import { carrierIso } from "../../lib/carrier";
import { CountryFlag } from "../CountryFlag";
import { SMSChannelStatusRow } from "./SMSChannelStatusRow";
export interface OverviewSimPanelProps {
device: DeviceDetail;
@@ -13,9 +14,10 @@ export interface OverviewSimPanelProps {
customPhoneNumber?: string;
e911Starting: boolean;
onSetupE911: () => void;
onRefreshOverview: () => void | Promise<void>;
}
export function OverviewSimPanel({ device, simOperatorDisplay, customPhoneNumber, e911Starting, onSetupE911 }: OverviewSimPanelProps) {
export function OverviewSimPanel({ device, simOperatorDisplay, customPhoneNumber, e911Starting, onSetupE911, onRefreshOverview }: OverviewSimPanelProps) {
const { t } = useI18n();
const [showSensitive, toggleSensitive] = useShowSensitive();
const modem = device.modem;
@@ -43,6 +45,7 @@ export function OverviewSimPanel({ device, simOperatorDisplay, customPhoneNumber
<FieldRow label="ICCID" value={modem?.iccid} sensitive={sensitive} monospace copyable />
<FieldRow label="IMSI" value={modem?.imsi} sensitive={sensitive} monospace copyable />
<FieldRow label={t("本机号码")} value={displayedPhoneNumber} sensitive={sensitive} monospace copyable />
<SMSChannelStatusRow device={device} onRefreshOverview={onRefreshOverview} />
{device?.e911SetupAvailable ? (
<div className="flex justify-between gap-3">
<span className="text-gray-500">{t("E911地址")}</span>
@@ -0,0 +1,115 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { ArrowSyncRegular } from "@fluentui/react-icons";
import { apiMessage } from "../../api";
import { Tag, type TagType } from "../ui/Tag";
import { message } from "../ui";
import { cx } from "../../lib/utils";
import { useI18n } from "../../lib/i18n";
import { getCellularIMS, type CellularIMSStatus } from "./deviceActions";
import { isDeviceOnline, isVoWiFiInUse } from "./shared";
import type { DeviceDetail } from "./types";
interface SMSChannelStatusRowProps {
device: DeviceDetail;
onRefreshOverview: () => void | Promise<void>;
}
interface DisplayStatus {
label: string;
tone: TagType;
}
export function SMSChannelStatusRow({ device, onRefreshOverview }: SMSChannelStatusRowProps) {
const { t } = useI18n();
const [cellular, setCellular] = useState<CellularIMSStatus | null>(null);
const [loading, setLoading] = useState(false);
const requestSequence = useRef(0);
const iccid = (device.modem?.iccid || "").trim();
const usesVoWiFi = isVoWiFiInUse(device) && !(device.modem?.imei && device.modem?.simInserted === false);
const canProbeCellular = !usesVoWiFi && isDeviceOnline(device) && !!iccid && device.modem?.simInserted !== false;
const probeCellular = useCallback(async () => {
if (!canProbeCellular) return;
const sequence = ++requestSequence.current;
setLoading(true);
try {
const next = await getCellularIMS(device.id);
if (sequence === requestSequence.current) setCellular(next);
} catch {
if (sequence === requestSequence.current) setCellular(null);
} finally {
if (sequence === requestSequence.current) setLoading(false);
}
}, [canProbeCellular, device.id]);
// The overview tab is conditionally mounted, so this runs once when users
// return to it and again when the active device/SIM/profile changes.
useEffect(() => {
requestSequence.current += 1;
setCellular(null);
setLoading(false);
if (canProbeCellular) void probeCellular();
return () => {
requestSequence.current += 1;
};
}, [canProbeCellular, device.id, iccid, device.activeEsimProfileName, usesVoWiFi, probeCellular]);
const refresh = async () => {
if (loading) return;
if (usesVoWiFi) {
const sequence = ++requestSequence.current;
setLoading(true);
try {
await onRefreshOverview();
} catch (error) {
if (sequence === requestSequence.current) {
message.error(apiMessage(error) || t("刷新短信通道状态失败"));
}
} finally {
if (sequence === requestSequence.current) setLoading(false);
}
return;
}
await probeCellular();
};
let display: DisplayStatus;
if (usesVoWiFi) {
if (device.vowifiRuntime?.smsReady) display = { label: t("已就绪(VoWiFi)"), tone: "success" };
else if (device.vowifiRuntime?.imsReady) display = { label: t("已注册(IMS)"), tone: "success" };
else if (device.vowifiRuntime?.enabled) display = { label: t("未注册短信域"), tone: "warning" };
else display = { label: t("状态未知"), tone: "info" };
} else if (!canProbeCellular || !cellular) {
display = { label: t("状态未知"), tone: "info" };
} else if (cellular.registered && cellular.csRegistered) {
display = { label: t("已注册(CS, IMS)"), tone: "success" };
} else if (cellular.registered) {
display = { label: t("已注册(IMS)"), tone: "success" };
} else if (cellular.csRegistered) {
display = { label: t("已注册(CS)"), tone: "success" };
} else if (cellular.csKnown && cellular.supported) {
display = { label: t("未注册短信域"), tone: "warning" };
} else {
display = { label: t("状态未知"), tone: "info" };
}
return (
<div className="flex w-full min-w-0 items-center justify-between gap-3">
<span className="shrink-0 whitespace-nowrap text-gray-500">{t("短信通道")}</span>
<div className="flex min-w-0 items-center justify-end gap-1.5">
<Tag type={display.tone}>{display.label}</Tag>
<button
type="button"
onClick={() => void refresh()}
disabled={loading}
title={t("刷新短信通道状态")}
aria-label={t("刷新短信通道状态")}
className="rounded p-1 text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-60 dark:hover:bg-white/10 dark:hover:text-gray-200"
>
<ArrowSyncRegular className={cx("h-4 w-4", loading && "animate-spin")} />
</button>
</div>
</div>
);
}
+9 -1
View File
@@ -1208,6 +1208,15 @@ export const EN_DICT: Record<string, string> = {
// Additional missing system strings
"端口": "Port",
"错误详情": "Error Details",
"短信通道": "SMS channel",
"刷新短信通道状态": "Refresh SMS channel status",
"刷新短信通道状态失败": "Failed to refresh SMS channel status",
"已注册(CS)": "Registered (CS)",
"已注册(IMS)": "Registered (IMS)",
"已注册(CS, IMS)": "Registered (CS, IMS)",
"已就绪(VoWiFi)": "Ready (VoWiFi)",
"未注册短信域": "No SMS domain registered",
"状态未知": "Unknown",
"正在搜索网络": "Searching network",
"SM-DP+ 的公开 Profile 库存已耗尽,请稍后重试或更换服务。":
"The public profile inventory on the SM-DP+ is exhausted. Please try again later or use a different service.",
@@ -1218,4 +1227,3 @@ export const EN_DICT: Record<string, string> = {
"已发现该模组,但未找到 AT 串口:通常是 option 驱动未认该 PID 或模组处于 MBIM/RNDIS 组态。可 ":
"Modem detected, but no AT serial port found: option driver may not recognize this PID or modem is in MBIM/RNDIS mode. You can ",
};