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,7 @@ import 'package:share_plus/share_plus.dart';
import 'package:sepot/core/l10n/error_messages.dart';
import 'package:sepot/core/l10n/l10n.dart';
import 'package:sepot/core/theme/app_theme.dart';
import 'package:sepot/models/identity.dart';
import 'package:sepot/providers/identity_provider.dart';
import 'package:sepot/providers/messaging_provider.dart';
@@ -27,12 +28,24 @@ class QrShareScreen extends ConsumerWidget {
return Center(child: Text(context.l10n.qrIdentityNotFound));
}
// Sdílet s routingem (accountId + delivery address z relaye),
// jinak protistrana nemá kam poslat první zprávu
// Sledovat stav připojení: po dokončení vlastní registrace se
// routing (aid+da) zapíše do DB a relay se připojí → tenhle watch
// obrazovku překreslí a QR se z „připojuji" přepne na hotový kód.
ref.watch(relayConnectionProvider);
final service = ref.watch(messagingServiceProvider);
final shareString = (service.shareableIdentity ??
identity.publicIdentity)
.toShareString();
final shareable = service.shareableIdentity;
final hasRouting = shareable != null &&
shareable.accountId != null &&
shareable.deliveryAddress != null;
// Bez routingu by QR/odkaz byl neúplný (protistrana by neměla kam
// poslat první zprávu → „Kontakt nemá doručovací adresu"). Radši
// než mrtvý kód ukázat stav připojování.
if (!hasRouting) {
return _ConnectingPlaceholder(identity: identity);
}
final shareString = shareable.toShareString();
return SingleChildScrollView(
padding: const EdgeInsets.all(32),
@@ -157,3 +170,56 @@ class QrShareScreen extends ConsumerWidget {
);
}
}
/// Stav obrazovky, dokud vlastní registrace na relay nedoběhne — místo
/// neúplného QR (bez routingu) ukáže identitu a hlášku o připojování.
class _ConnectingPlaceholder extends StatelessWidget {
const _ConnectingPlaceholder({required this.identity});
final LocalIdentity identity;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(color: SepotColors.ember),
const SizedBox(height: 32),
Text(
context.l10n.qrShareConnectingTitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: kFontSans,
fontSize: 20,
fontWeight: FontWeight.w700,
color: SepotColors.textPrimary,
),
),
const SizedBox(height: 12),
Text(
context.l10n.qrShareConnectingBody,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: SepotColors.textSecondary,
),
),
const SizedBox(height: 24),
Text(
identity.sepotId,
style: const TextStyle(
fontFamily: kFontMono,
fontSize: 20,
fontWeight: FontWeight.w600,
color: SepotColors.ember,
),
),
],
),
),
);
}
}