hostDialerPresentation function
- WalletLocalizations l10n, {
- required String name,
- required IsolationSupport isolation,
- required TransportExposure exposure,
- required HostDialerSentences sentences,
The presentation of
TorState.active(runtime: hostDialer(name, isolation, exposure)) (FR-29
spec §3.4, ADR-0547 — the host NAMES its transport; the SDK has no list):
the chip carries the host's own name VERBATIM ("via your app's private
path (Tor)", "… (VLESS via Cloudflare)"), never a name the wallet chose;
an EMPTY name (only the SDK's unattributed rendering produces one) reads
as the locale's "a private path". exposure decides privacy: exposed
renders "not private (your app's direct connection)" — CAUTION tone since
FR-30 (c), the direct-connection explanation — whatever the isolation
says; unknown (not declared, or a value THIS binding cannot read)
renders linkable with
the caution tone and the unverified explanation, whatever the isolation
says (the wave review's MEDIUM: never the confident tone for a path the
wallet cannot vouch for); hidden renders the private path, and only
hidden + isolation supported earns the protected tone (§3.3 privacy
rule) — unsupported OR unknown isolation adds "; connections can be
linked by the proxy" with the caution tone (the state never promises
what the host did not declare).
SHARED WITH TorState.unanswered since FR-44, through sentences: that
state carries this exact payload, and the privacy verdict it implies must
not depend on which of the two arms is reading it. The exposure switch
below is the ONE place that decides it; each arm then picks its family's
words. The unanswered family is never TransportTone.protected — nothing
is carrying — so its tone cannot be what encodes the verdict, and its
SENTENCES do.
Implementation
TransportPresentation hostDialerPresentation(
WalletLocalizations l10n, {
required String name,
required IsolationSupport isolation,
required TransportExposure exposure,
required HostDialerSentences sentences,
}) {
// An EMPTY name is the SDK's own unattributed rendering (a host's empty
// name never crosses — the crossing refuses it): the locale's "a private
// path". Only the arms that name the path read it.
String transport() => name.isEmpty ? l10n.walletTorHostOtherTransport : name;
// The unanswered family keeps `copy`'s named/unnamed SENTENCE split on the
// one arm that has both (it reads "Private path connected" where there is
// no name to show). That is a NAMING choice, not a privacy one — the
// privacy verdict is the switch below, and it is the same either way.
final named = name.isEmpty ? null : name;
switch (exposure) {
// FR-30 (c), C1: CAUTION, not neutral. The arms were each defensible
// alone and the ORDERING a user read across them was not: the path that
// shows the server this device's address was the calm colour while the
// path that hides it — and only lets the proxy operator link the wallet's
// own connections — was the alarmed one, so a person reading colour
// concluded the direct connection was the safer choice. A privacy loss is
// never the calm colour; exposed and linkable now both read as caution and
// only hidden + isolated earns the protected tone. UI-only: the header
// pins the linkable SENTENCE, never the tone.
case TransportExposure.exposed:
// The name is DISCARDED on both families: what matters is that the
// server sees this device's address, whatever the host called the path.
return switch (sentences) {
HostDialerSentences.active => TransportPresentation(
label: l10n.walletTorHostDirect,
tone: TransportTone.caution,
detail: l10n.walletTransportExplainDirect,
),
HostDialerSentences.unanswered => TransportPresentation(
label: l10n.walletTorUnansweredDirect,
tone: TransportTone.caution,
detail: l10n.walletTransportExplainUnansweredDirect,
),
};
case TransportExposure.unknown:
return switch (sentences) {
HostDialerSentences.active => TransportPresentation(
label: l10n.walletTorHostPathLinkable(transport()),
tone: TransportTone.caution,
detail: l10n.walletTransportExplainUnverified,
),
HostDialerSentences.unanswered => TransportPresentation(
label: l10n.walletTorUnansweredLinkable(transport()),
tone: TransportTone.caution,
detail: l10n.walletTransportExplainUnansweredUnverified,
),
};
case TransportExposure.hidden:
final isolated = isolation == IsolationSupport.supported;
return switch (sentences) {
HostDialerSentences.active => TransportPresentation(
label: isolated
? l10n.walletTorHostPath(transport())
: l10n.walletTorHostPathLinkable(transport()),
tone: isolated ? TransportTone.protected : TransportTone.caution,
detail: l10n.walletTransportExplainHostProxy,
),
// The linkability sentence rides this family too: it is a fact about
// the path the host declared, and the path does not stop being
// linkable because it stopped answering. Only the protected tone is
// withheld — `Active`'s one green arm has no twin here.
HostDialerSentences.unanswered => TransportPresentation(
label: isolated
? (named == null
? l10n.walletTorUnanswered
: l10n.walletTorUnansweredNamed(named))
: l10n.walletTorUnansweredLinkable(transport()),
tone: TransportTone.caution,
detail: named == null
? l10n.walletTransportExplainUnanswered
: l10n.walletTransportExplainUnansweredNamed(named),
),
};
}
}