feat(#630): show how far a contact identity is confirmed

Adds a calm three-state indicator beside each contact name, per the
owner steer: a green check for advert-verified, a neutral outline check
for key-confirmed, a muted key for key-only. No amber and no hazard
glyph, because nothing is wrong with a key-added contact. The scale
reads as how much we know, never as how risky.

The states are not cosmetic:

- advertVerified: a signed advert arrived, so the node itself asserted
  its name, type and position, and the raw packet is stored, which is
  also what makes the contact re-shareable.
- keyConfirmed: a message with this contact went through. Direct
  messages are encrypted with an ECDH secret derived from the contact
  key, and the ACK is computed over the decrypted plaintext
  (BaseChatMesh.cpp:442,451), so this is proof the holder of the
  matching private key is live. It does NOT prove the person is who the
  name claims.
- keyOnly: someone supplied a key and nothing has confirmed it on air.

Costs almost nothing to compute. Contact.isAdvertVerified falls out of
the epoch last_advert_timestamp that #627 already writes, and it clears
itself when a real advert rewrites the field. The advert check runs
first so only an unconfirmed contact pays for a message scan, which
keeps a long contact list cheap.

Also fixes a defect the epoch sentinel introduced: _formatLastSeen ran
the epoch through the relative formatter and claimed a key-added contact
was last seen tens of thousands of days ago. It now reads Not heard yet.

Epic #619.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/619-contact-identity-uri
Strycher 3 weeks ago
parent 3fa5b4ee2f
commit 2643608e2d

@ -2517,6 +2517,10 @@
"contacts_scanContactQr": "Scan contact QR",
"contacts_scanContactQrInstructions": "Point the camera at a MeshCore contact QR",
"contacts_invalidContactQr": "That QR is not a MeshCore contact",
"contacts_verifiedByAdvert": "Confirmed on air. This node sent a signed advert.",
"contacts_verifiedByMessage": "Key confirmed. A message with this contact went through, which only works with the matching key.",
"contacts_verifiedKeyOnly": "Added from a key. Nothing has confirmed it on air yet.",
"contacts_lastSeenNever": "Not heard yet",
"contacts_ShareContact": "Copy contact to Clipboard",
"contacts_ShareContactZeroHop": "Share contact by advert",
"contacts_zeroHopContactAdvertSent": "Sent contact by advert.",

@ -7552,6 +7552,30 @@ abstract class AppLocalizations {
/// **'That QR is not a MeshCore contact'**
String get contacts_invalidContactQr;
/// No description provided for @contacts_verifiedByAdvert.
///
/// In en, this message translates to:
/// **'Confirmed on air. This node sent a signed advert.'**
String get contacts_verifiedByAdvert;
/// No description provided for @contacts_verifiedByMessage.
///
/// In en, this message translates to:
/// **'Key confirmed. A message with this contact went through, which only works with the matching key.'**
String get contacts_verifiedByMessage;
/// No description provided for @contacts_verifiedKeyOnly.
///
/// In en, this message translates to:
/// **'Added from a key. Nothing has confirmed it on air yet.'**
String get contacts_verifiedKeyOnly;
/// No description provided for @contacts_lastSeenNever.
///
/// In en, this message translates to:
/// **'Not heard yet'**
String get contacts_lastSeenNever;
/// No description provided for @contacts_ShareContact.
///
/// In en, this message translates to:

@ -4410,6 +4410,21 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Копирай контакт в клипборда';

@ -4422,6 +4422,21 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Kontakt in die Zwischenablage kopieren';

@ -4344,6 +4344,21 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Copy contact to Clipboard';

@ -4411,6 +4411,21 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Copiar contacto al Portapapeles';

@ -4433,6 +4433,21 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact =>
'Copier le contact dans le presse-papiers';

@ -4428,6 +4428,21 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Másolja a kapcsolatot a kiválasztóba';

@ -4416,6 +4416,21 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Copia contatto negli Appunti';

@ -4213,6 +4213,21 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => '連絡先をクリップボードにコピー';

@ -4215,6 +4215,21 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => '연락처를 복사';

@ -4396,6 +4396,21 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Contact naar Klembord kopiëren';

@ -4426,6 +4426,21 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Kopiuj kontakt do schowka';

@ -4408,6 +4408,21 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact =>
'Copiar contato para Área de Transferência';

@ -4418,6 +4418,21 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Копировать контакт в буфер обмена';

@ -4392,6 +4392,21 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Kopírovať kontakt do schránky';

@ -4386,6 +4386,21 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Kopiraj stik v Odložišče';

@ -4365,6 +4365,21 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Kopiera kontakt till Urklipp';

@ -4417,6 +4417,21 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => 'Копіювати контакт у буфер обміну';

@ -4116,6 +4116,21 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contacts_invalidContactQr => 'That QR is not a MeshCore contact';
@override
String get contacts_verifiedByAdvert =>
'Confirmed on air. This node sent a signed advert.';
@override
String get contacts_verifiedByMessage =>
'Key confirmed. A message with this contact went through, which only works with the matching key.';
@override
String get contacts_verifiedKeyOnly =>
'Added from a key. Nothing has confirmed it on air yet.';
@override
String get contacts_lastSeenNever => 'Not heard yet';
@override
String get contacts_ShareContact => '复制联系人信息到剪贴板';

@ -260,6 +260,15 @@ class Contact {
}
}
/// True once this contact has been confirmed on air by a signed advert.
///
/// [lastSeen] maps to the firmware `last_advert_timestamp`, which a contact
/// created from a bare key deliberately carries as the epoch so the advert
/// replay guard cannot mute it (#627). That same sentinel doubles as the
/// verification signal, for free, and it clears itself the moment a genuine
/// advert arrives and the radio rewrites the field. (#630)
bool get isAdvertVerified => lastSeen.millisecondsSinceEpoch != 0;
/// Reference-app contact share URI for this contact, the inverse of
/// [fromShareUri]. This is what the stock app accepts, so emitting it is
/// what makes Offband cards and QRs importable by non-Offband users. (#626)

@ -35,6 +35,7 @@ import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart';
import '../widgets/sync_progress_overlay.dart';
import '../widgets/add_contact_by_key_dialog.dart';
import '../widgets/contact_verification_badge.dart';
import '../widgets/my_contact_qr_dialog.dart';
import '../widgets/unread_badge.dart';
import '../helpers/snack_bar_builder.dart';
@ -1641,7 +1642,22 @@ class _ContactTile extends StatelessWidget {
),
],
)
: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
: Row(
children: [
// Reads as a column of state down the list. Calm by design:
// a key-added contact is not a problem, just less confirmed
// (#630).
ContactVerificationBadge(contact: contact),
const SizedBox(width: 6),
Expanded(
child: Text(
contact.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -1747,6 +1763,14 @@ class _ContactTile extends StatelessWidget {
}
String _formatLastSeen(BuildContext context, DateTime lastSeen) {
// A contact added from a bare key carries the epoch deliberately, so the
// firmware advert replay guard cannot mute it (#627). Rendering that
// through the relative formatter would claim it was last seen tens of
// thousands of days ago, which is worse than saying nothing. (#630)
if (lastSeen.millisecondsSinceEpoch == 0) {
return context.l10n.contacts_lastSeenNever;
}
final now = DateTime.now();
final diff = now.difference(lastSeen);

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../models/message.dart';
/// How far a contact identity has actually been confirmed. (#630)
enum ContactVerification {
/// Added from a bare key. Nothing has confirmed it on air.
keyOnly,
/// A message with this contact went through. That is cryptographic proof
/// the holder of the matching private key is live and reachable, because
/// direct messages are encrypted with an ECDH secret derived from this
/// contact key, and the ACK is computed over the decrypted plaintext
/// (firmware `BaseChatMesh.cpp:442,451`). It does NOT prove the person is
/// who the name claims: names are display, keys are identity.
keyConfirmed,
/// A signed advert has been received. Strongest state: the node itself
/// asserted its name, type and position, and the raw advert is stored, so
/// the contact can also be re-shared.
advertVerified,
}
/// Resolves the verification state for [contact].
///
/// The advert check is first because it is free and covers most contacts; only
/// an unverified contact pays for a message scan, which keeps this cheap on a
/// long contact list.
ContactVerification resolveContactVerification(
Contact contact,
MeshCoreConnector connector,
) {
if (contact.isAdvertVerified) return ContactVerification.advertVerified;
final delivered = connector
.getMessages(contact)
.any((m) => m.status == MessageStatus.delivered);
return delivered
? ContactVerification.keyConfirmed
: ContactVerification.keyOnly;
}
/// A small, deliberately calm indicator of how far a contact is confirmed.
///
/// Owner steer (#630): a green check for fully verified and a different icon
/// otherwise. Explicitly NOT amber and NOT a hazard glyph, because nothing is
/// wrong with a key-added contact. The scale reads as "how much we know",
/// never as "how risky".
class ContactVerificationBadge extends StatelessWidget {
const ContactVerificationBadge({
super.key,
required this.contact,
this.size = 15,
});
final Contact contact;
final double size;
@override
Widget build(BuildContext context) {
final state = resolveContactVerification(
contact,
context.read<MeshCoreConnector>(),
);
final scheme = Theme.of(context).colorScheme;
final l10n = context.l10n;
final (IconData icon, Color color, String tooltip) = switch (state) {
ContactVerification.advertVerified => (
Icons.verified,
// The one deliberately positive colour in the set.
Colors.green,
l10n.contacts_verifiedByAdvert,
),
ContactVerification.keyConfirmed => (
Icons.check_circle_outline,
scheme.onSurfaceVariant,
l10n.contacts_verifiedByMessage,
),
ContactVerification.keyOnly => (
Icons.key_outlined,
scheme.onSurfaceVariant,
l10n.contacts_verifiedKeyOnly,
),
};
return Tooltip(
message: tooltip,
child: Icon(icon, size: size, color: color),
);
}
}

@ -0,0 +1,171 @@
// Verification-state tests (#630).
//
// The three states are not cosmetic. advertVerified means the node itself
// asserted its identity in a signed advert. keyConfirmed means a message went
// through, which only works with the matching private key. keyOnly means
// someone typed a key and nothing has confirmed it on air.
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/l10n/app_localizations.dart';
import 'package:meshcore_open/models/contact.dart';
import 'package:meshcore_open/models/message.dart';
import 'package:meshcore_open/widgets/contact_verification_badge.dart';
const _key = '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff';
Contact _contact({required DateTime lastSeen}) => Contact(
publicKey: hex2Uint8List(_key),
name: 'Bob',
type: advTypeChat,
pathLength: -1,
path: Uint8List(0),
lastSeen: lastSeen,
);
Contact _keyOnly() =>
_contact(lastSeen: DateTime.fromMillisecondsSinceEpoch(0));
Contact _adverted() => _contact(lastSeen: DateTime(2026, 9, 6, 12));
Message _msg(MessageStatus status) => Message(
senderKey: hex2Uint8List(_key),
text: 'hi',
isOutgoing: true,
timestamp: DateTime(2026, 9, 6),
status: status,
);
class _Conn extends MeshCoreConnector {
_Conn(this.messages);
final List<Message> messages;
@override
List<Message> getMessages(Contact contact) => messages;
}
void main() {
group('Contact.isAdvertVerified (#630)', () {
test('a key-only contact carries the epoch and is not advert-verified', () {
expect(_keyOnly().isAdvertVerified, isFalse);
});
test('any real advert timestamp counts as verified', () {
expect(_adverted().isAdvertVerified, isTrue);
});
test('the parser output is unverified by construction', () {
// Ties the model getter to what fromShareUri actually produces, so the
// two cannot drift.
final parsed = Contact.fromShareUri(
'meshcore://contact/add?name=Bob&public_key=$_key&type=1',
)!;
expect(parsed.isAdvertVerified, isFalse);
});
});
group('resolveContactVerification (#630)', () {
test('an adverted contact is advertVerified even with no messages', () {
expect(
resolveContactVerification(_adverted(), _Conn(const [])),
ContactVerification.advertVerified,
);
});
test('an advert wins over message history', () {
// The advert is strictly stronger: it is signed and it stores the raw
// packet that makes the contact re-shareable.
expect(
resolveContactVerification(
_adverted(),
_Conn([_msg(MessageStatus.delivered)]),
),
ContactVerification.advertVerified,
);
});
test('a key-only contact with no traffic is keyOnly', () {
expect(
resolveContactVerification(_keyOnly(), _Conn(const [])),
ContactVerification.keyOnly,
);
});
test('a delivered message upgrades a key-only contact to keyConfirmed', () {
expect(
resolveContactVerification(
_keyOnly(),
_Conn([_msg(MessageStatus.sent), _msg(MessageStatus.delivered)]),
),
ContactVerification.keyConfirmed,
);
});
test('unacked traffic does NOT confirm the key', () {
// sent means it left the radio. failed and pending prove nothing at all.
// Only delivered means the far end produced the right ACK, which needs
// the plaintext, which needs the matching private key.
for (final s in [
MessageStatus.pending,
MessageStatus.sent,
MessageStatus.failed,
]) {
expect(
resolveContactVerification(_keyOnly(), _Conn([_msg(s)])),
ContactVerification.keyOnly,
reason: '$s must not count as confirmation',
);
}
});
});
group('ContactVerificationBadge rendering (#630)', () {
Future<void> pump(WidgetTester tester, Contact c, _Conn conn) =>
tester.pumpWidget(
ChangeNotifierProvider<MeshCoreConnector>.value(
value: conn,
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(body: ContactVerificationBadge(contact: c)),
),
),
);
testWidgets('advert-verified shows the green check', (tester) async {
await pump(tester, _adverted(), _Conn(const []));
final icon = tester.widget<Icon>(find.byType(Icon));
expect(icon.icon, Icons.verified);
expect(icon.color, Colors.green);
});
testWidgets('key-confirmed shows a neutral check, not green', (
tester,
) async {
await pump(tester, _keyOnly(), _Conn([_msg(MessageStatus.delivered)]));
final icon = tester.widget<Icon>(find.byType(Icon));
expect(icon.icon, Icons.check_circle_outline);
expect(icon.color, isNot(Colors.green));
});
testWidgets('key-only shows a muted key and nothing alarming', (
tester,
) async {
await pump(tester, _keyOnly(), _Conn(const []));
final icon = tester.widget<Icon>(find.byType(Icon));
expect(icon.icon, Icons.key_outlined);
// Owner steer: no amber, no hazard glyph. Nothing is wrong with this
// contact, it is just less confirmed.
expect(icon.icon, isNot(Icons.warning));
expect(icon.icon, isNot(Icons.warning_amber));
expect(icon.icon, isNot(Icons.error_outline));
expect(icon.color, isNot(Colors.amber));
expect(icon.color, isNot(Colors.red));
});
});
}

@ -111,6 +111,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -248,6 +252,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -385,6 +393,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -522,6 +534,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -659,6 +675,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -796,6 +816,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -933,6 +957,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1070,6 +1098,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1207,6 +1239,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1344,6 +1380,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1481,6 +1521,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1618,6 +1662,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1755,6 +1803,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -1892,6 +1944,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -2029,6 +2085,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -2166,6 +2226,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",
@ -2303,6 +2367,10 @@
"contacts_scanContactQr",
"contacts_scanContactQrInstructions",
"contacts_invalidContactQr",
"contacts_verifiedByAdvert",
"contacts_verifiedByMessage",
"contacts_verifiedKeyOnly",
"contacts_lastSeenNever",
"snrIndicator_noNeighbors",
"block_block",
"block_unblock",

Loading…
Cancel
Save

Powered by TurnKey Linux.