fix: reliable first-message send, QR routing, auto-lock recovery

Fixes reported by testers and found in an app review:

- First message right after install no longer fails. Before the app
  finished registering with the relay, the outgoing message could crash on
  a null delivery address. The profile field is now optional and the send
  path throws a clear "still connecting, try again shortly" error while
  registration completes in the background.
- The QR code and share link now appear only once the delivery address is
  ready. Previously a link generated before the first connection lacked
  routing, so the other side could not send the first message. The screen
  now shows a connecting state and reactively swaps in the QR when ready.
- Incomplete contact links are rejected on import instead of failing at
  first send, with a clear prompt to ask for a fresh link.
- Auto-lock no longer silences the app: after locking and unlocking,
  messaging is restarted (it previously stayed dead until app restart).
- Group messages are marked failed on a non-per-member send error instead
  of hanging in the "sending" state forever.
- Hardened the relay client against a rare duplicate connection when
  toggling Tor or locking mid-connect, and fromShareString now rejects a
  delivery address of the wrong length.
- Pin unifiedpush below 6.2.0 to drop the webcrypto dependency (its native
  framework lacks LC_ENCRYPTION_INFO and blocked the iOS upload).
- Skip Android release lint (network dependency check hangs on the build
  host and adds no value for a Flutter app).
This commit is contained in:
Julius Joska
2026-07-18 19:53:41 +02:00
parent 7d7f589fc5
commit 90a1390716
29 changed files with 403 additions and 162 deletions
@@ -6,6 +6,8 @@ import 'package:local_auth/local_auth.dart';
import 'package:sepot/core/crypto/dek_wrap.dart';
import 'package:sepot/core/l10n/l10n.dart';
import 'package:sepot/core/theme/app_theme.dart';
import 'package:sepot/providers/messaging_provider.dart';
import 'package:sepot/providers/push_provider.dart';
import 'package:sepot/providers/storage_provider.dart';
import 'package:sepot/widgets/sepot_logo.dart';
@@ -43,6 +45,7 @@ class _PinLockScreenState extends ConsumerState<PinLockScreen> {
);
if (!ok) return; // uživatel zrušil → zůstává PIN
await storage.unlockWithBiometrics();
_restartMessagingAfterUnlock();
if (mounted) context.go('/chats');
} catch (_) {
// biometrie nedostupná/selhala — PIN je vždy fallback
@@ -211,6 +214,16 @@ class _PinLockScreenState extends ConsumerState<PinLockScreen> {
});
}
/// Auto-lock službu zastavil a storage zamkl; po odemčení je nutné
/// messaging znovu nastartovat. messagingStartProvider drží starou
/// „hotovou" hodnotu (start se sám neprovede) → invalidace ho přepočítá
/// nad odemčeným storage a spustí start() znovu (jinak žádný příjem ani
/// odesílání až do restartu appky).
void _restartMessagingAfterUnlock() {
ref.invalidate(messagingStartProvider);
ref.invalidate(pushInitProvider);
}
Future<void> _verifyPin() async {
setState(() => _isVerifying = true);
@@ -218,6 +231,7 @@ class _PinLockScreenState extends ConsumerState<PinLockScreen> {
// „Ověření" PINu = Argon2id unwrap DEK + otevření SQLCipher DB.
// Špatný PIN = GCM tag nesedí = DB zůstává nečitelná.
await ref.read(storageProvider).unlockWithPin(_pin);
_restartMessagingAfterUnlock();
if (mounted) context.go('/chats');
} on WrongPinException {
setState(() {