8081d98c67
Initial public release. Anonymous on-device identity (no phone number, e-mail, or password), Signal-protocol cryptography (X3DH + PQXDH, Double Ratchet, sealed sender) via libsignal, a metadata-minimizing relay, group chats, disappearing messages, encrypted local storage, optional Tor transport, and a UI localized into Czech, English, German, Russian, and Italian. Monorepo: Flutter client (app/), Rust relay + wire types (rust/), protobuf definitions (proto/), self-host deploy (deploy/). Licensed under AGPL-3.0 with an app-store distribution exception (§7).
242 lines
8.6 KiB
Dart
242 lines
8.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:intl/date_symbol_data_local.dart';
|
|
import 'package:sepot/app.dart';
|
|
import 'package:sepot/core/constants/app_constants.dart';
|
|
import 'package:sepot/core/crypto/sepot_crypto.dart';
|
|
import 'package:sepot/core/l10n/l10n.dart';
|
|
import 'package:sepot/core/push/push_bootstrap.dart';
|
|
|
|
/// Proces nastartoval Android kvůli pushi, bez UI — `runApp` se nesmí zavolat
|
|
/// ani ze záchranného handleru.
|
|
bool _bezUi = false;
|
|
|
|
/// Reálné UI už běží. Pozdější async chyba ho nesmí přepsat chybovou obrazovkou.
|
|
bool _uiNabehla = false;
|
|
|
|
/// Start aplikace.
|
|
///
|
|
/// KRITICKÉ: mezi startem a prvním `runApp()` nesmí zůstat nechráněné `await`.
|
|
/// Když nativní init (libsignal) na zařízení selže, appka nesmí skončit němou
|
|
/// bílou obrazovkou — místo toho ukáže chybu, aby šla diagnostikovat.
|
|
void main(List<String> args) {
|
|
runZonedGuarded(() async {
|
|
// ÚPLNĚ PRVNÍ: kdyby cokoli níž (i ensureInitialized) hodilo, záchranný
|
|
// handler musí vědět, že v headless push startu runApp volat nesmí.
|
|
// Čteme z args nezávisle na PushBootstrap, aby to platilo i kdyby jeho
|
|
// init selhal.
|
|
_bezUi = args.contains('--unifiedpush-bg');
|
|
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
// Bez tohohle vykreslí release build chybu v build() jako němou šedou
|
|
// plochu bez textu. V debug si necháme červenou obrazovku Flutteru.
|
|
if (kReleaseMode) {
|
|
ErrorWidget.builder =
|
|
(details) => _ChybovaPlocha(detail: details.exceptionAsString());
|
|
|
|
// network_security_config hlídá jen platformní HTTP stack, ne dart:io
|
|
// sockety. `--dart-define=SEPOT_RELAY=http://…` by tedy v release
|
|
// buildu tiše poslal provoz v plaintextu. Radši tvrdý pád.
|
|
final relay = Uri.parse(AppConstants.relayUrl);
|
|
if (relay.scheme != 'https' && relay.scheme != 'wss') {
|
|
throw StateError(
|
|
'Release build must talk https/wss, not "${relay.scheme}".',
|
|
);
|
|
}
|
|
}
|
|
|
|
// Chyby render pipeline zalogovat, ale NEshazovat kvůli nim běžící UI.
|
|
FlutterError.onError = (details) {
|
|
FlutterError.presentError(details);
|
|
debugPrint('FlutterError: ${details.exceptionAsString()}');
|
|
};
|
|
|
|
// Formáty dat všech podporovaných jazyků — nekritické, appka poběží i bez nich
|
|
for (final lang in const ['cs', 'en', 'de', 'ru', 'it']) {
|
|
try {
|
|
await initializeDateFormatting(lang);
|
|
} catch (e, s) {
|
|
debugPrint('initializeDateFormatting($lang) selhalo (nekritické): $e\n$s');
|
|
}
|
|
}
|
|
|
|
// UnifiedPush callbacky VŽDY před runApp — Android budí zabitou appku
|
|
// s `--unifiedpush-bg` bez UI (killed-state push budíček).
|
|
try {
|
|
await PushBootstrap.init(args);
|
|
} catch (e, s) {
|
|
// Push je nekritický: bez něj appka funguje, jen nechodí budíčky.
|
|
debugPrint('PushBootstrap.init selhalo (nekritické): $e\n$s');
|
|
}
|
|
if (_bezUi || PushBootstrap.backgroundStart) {
|
|
// Bez UI: onMessage jen ukáže generickou notifikaci (DB je zamčená,
|
|
// dešifrovat nejde). Engine drží plugin, runApp se NEvolá.
|
|
return;
|
|
}
|
|
|
|
// Nativní krypto backend (libsignal) — musí běžet dřív než cokoli,
|
|
// co sahá na identitu. Nejčastější příčina bílé obrazovky na iOS.
|
|
// Timeout kryje i zásek (init nevrátí) — nesmí viset donekonečna.
|
|
try {
|
|
await SepotCrypto.init().timeout(const Duration(seconds: 15));
|
|
} catch (e, s) {
|
|
debugPrint('SepotCrypto.init selhalo: $e\n$s');
|
|
// Chybová cesta: keystore může být sám rozbitý, uloženou volbu jazyka
|
|
// nečteme — jen systémový jazyk.
|
|
runApp(_ChybaApp(
|
|
titulek: l10nFor(null).errorCryptoInitTitle,
|
|
detail: '$e',
|
|
));
|
|
return;
|
|
}
|
|
|
|
// Zamkni orientaci na portrét
|
|
await SystemChrome.setPreferredOrientations([
|
|
DeviceOrientation.portraitUp,
|
|
]);
|
|
|
|
// Tmavý status bar
|
|
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
|
statusBarColor: Colors.transparent,
|
|
statusBarIconBrightness: Brightness.light,
|
|
systemNavigationBarColor: Color(0xFF0B0C12),
|
|
systemNavigationBarIconBrightness: Brightness.light,
|
|
));
|
|
|
|
// Úložiště startuje ZAMČENÉ — odemyká se až v loading/pin-lock flow
|
|
_spustUi(const ProviderScope(child: SepotApp()));
|
|
}, (error, stack) {
|
|
debugPrint('Neodchycená chyba při startu: $error\n$stack');
|
|
// Headless push nemá UI, kam chybu vykreslit.
|
|
if (_bezUi) return;
|
|
// Appka už jede — pozdější async chyba ji nesmí shodit do chybovky.
|
|
if (_uiNabehla) return;
|
|
runApp(_ChybaApp(titulek: l10nFor(null).errorStartupTitle, detail: '$error'));
|
|
});
|
|
}
|
|
|
|
void _spustUi(Widget app) {
|
|
_uiNabehla = true;
|
|
runApp(app);
|
|
}
|
|
|
|
/// Náhrada za výchozí `ErrorWidget` — ten v release módu kreslí prázdnou
|
|
/// šedou plochu bez jediného slova. Nesmí spoléhat na `MaterialApp` nad sebou.
|
|
class _ChybovaPlocha extends StatelessWidget {
|
|
final String detail;
|
|
|
|
const _ChybovaPlocha({required this.detail});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: ColoredBox(
|
|
color: const Color(0xFF0B0C12),
|
|
child: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10nFor(null).errorScreenNotRenderedTitle,
|
|
style: const TextStyle(
|
|
color: Color(0xFFF0EEE8),
|
|
fontSize: 19,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
SelectableText(
|
|
detail,
|
|
style: const TextStyle(
|
|
color: Color(0xFFF0795A),
|
|
fontFamily: 'monospace',
|
|
fontSize: 12,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Minimální chybová obrazovka — bez závislosti na krypto/providerech,
|
|
/// aby se vykreslila i když selže start. Text jde označit a zkopírovat.
|
|
class _ChybaApp extends StatelessWidget {
|
|
final String titulek;
|
|
final String detail;
|
|
|
|
const _ChybaApp({required this.titulek, required this.detail});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
home: Scaffold(
|
|
backgroundColor: const Color(0xFF0B0C12),
|
|
body: SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(28),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 40),
|
|
Text(
|
|
titulek,
|
|
style: const TextStyle(
|
|
color: Color(0xFFF0EEE8),
|
|
fontSize: 25,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
l10nFor(null).errorStartupBody,
|
|
style: const TextStyle(color: Color(0xFF9AA0B5), fontSize: 15, height: 1.5),
|
|
),
|
|
const SizedBox(height: 22),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF12141D),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFF262A3A)),
|
|
),
|
|
child: SelectableText(
|
|
detail,
|
|
style: const TextStyle(
|
|
color: Color(0xFFF0795A),
|
|
fontFamily: 'monospace',
|
|
fontSize: 12.5,
|
|
height: 1.55,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text(
|
|
l10nFor(null).errorContactFooter,
|
|
style: const TextStyle(color: Color(0xFF626880), fontSize: 12.5),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|