+2
-27
@@ -1,33 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'src/app.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ProviderScope(child: BaronSafeApp()));
|
||||
}
|
||||
|
||||
class BaronSafeApp extends StatelessWidget {
|
||||
const BaronSafeApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Baron Safe',
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: const BaronSafeHome(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BaronSafeHome extends StatelessWidget {
|
||||
const BaronSafeHome({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Baron Safe')),
|
||||
body: const Center(
|
||||
child: Text('Baron Safe hybrid app scaffold'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'features/home/home_screen.dart';
|
||||
import 'features/webview/baron_safe_webview_screen.dart';
|
||||
|
||||
final _router = GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
name: HomeScreen.routeName,
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/safe',
|
||||
name: BaronSafeWebViewScreen.routeName,
|
||||
builder: (context, state) => const BaronSafeWebViewScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
class BaronSafeApp extends StatelessWidget {
|
||||
const BaronSafeApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'Baron Safe',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF2563EB),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF60A5FA),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
routerConfig: _router,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
enum BaronSafeBridgeCommand {
|
||||
getDeviceInfo('getDeviceInfo'),
|
||||
registerPushToken('registerPushToken'),
|
||||
openBiometricPrompt('openBiometricPrompt'),
|
||||
blockSession('blockSession'),
|
||||
blockLinkedApp('blockLinkedApp'),
|
||||
openSettings('openSettings');
|
||||
|
||||
const BaronSafeBridgeCommand(this.value);
|
||||
|
||||
final String value;
|
||||
|
||||
static BaronSafeBridgeCommand? tryParse(String value) {
|
||||
for (final command in values) {
|
||||
if (command.value == value) {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'baron_safe_bridge_command.dart';
|
||||
|
||||
class BaronSafeBridgeRequest {
|
||||
const BaronSafeBridgeRequest({
|
||||
required this.command,
|
||||
this.payload = const <String, Object?>{},
|
||||
});
|
||||
|
||||
final BaronSafeBridgeCommand command;
|
||||
final Map<String, Object?> payload;
|
||||
}
|
||||
|
||||
class BaronSafeBridgeResponse {
|
||||
const BaronSafeBridgeResponse.success({this.data = const <String, Object?>{}})
|
||||
: error = null;
|
||||
|
||||
const BaronSafeBridgeResponse.failure(this.error)
|
||||
: data = const <String, Object?>{};
|
||||
|
||||
final Map<String, Object?> data;
|
||||
final BaronSafeBridgeError? error;
|
||||
|
||||
bool get isSuccess => error == null;
|
||||
}
|
||||
|
||||
class BaronSafeBridgeError {
|
||||
const BaronSafeBridgeError({required this.code, required this.message});
|
||||
|
||||
final String code;
|
||||
final String message;
|
||||
}
|
||||
|
||||
class BaronSafeDeviceInfo {
|
||||
const BaronSafeDeviceInfo({
|
||||
required this.deviceId,
|
||||
required this.platform,
|
||||
required this.appVersion,
|
||||
});
|
||||
|
||||
final String deviceId;
|
||||
final String platform;
|
||||
final String appVersion;
|
||||
|
||||
Map<String, Object?> toJson() {
|
||||
return {
|
||||
'deviceId': deviceId,
|
||||
'platform': platform,
|
||||
'appVersion': appVersion,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BaronSafeBiometricResult {
|
||||
const BaronSafeBiometricResult({
|
||||
required this.verified,
|
||||
required this.reason,
|
||||
});
|
||||
|
||||
final bool verified;
|
||||
final String reason;
|
||||
|
||||
Map<String, Object?> toJson() {
|
||||
return {'verified': verified, 'reason': reason};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'baron_safe_bridge_command.dart';
|
||||
import 'baron_safe_bridge_models.dart';
|
||||
import '../push/baron_safe_push_service.dart';
|
||||
|
||||
abstract interface class BaronSafeBridgeService {
|
||||
Future<BaronSafeBridgeResponse> handle(BaronSafeBridgeRequest request);
|
||||
}
|
||||
|
||||
abstract interface class BaronSafeDeviceInfoProvider {
|
||||
Future<BaronSafeDeviceInfo> readDeviceInfo();
|
||||
}
|
||||
|
||||
abstract interface class BaronSafeBiometricPrompt {
|
||||
Future<BaronSafeBiometricResult> verify({required String reason});
|
||||
}
|
||||
|
||||
class PlaceholderBaronSafeBridgeService implements BaronSafeBridgeService {
|
||||
const PlaceholderBaronSafeBridgeService({
|
||||
this.deviceInfoProvider = const PlaceholderBaronSafeDeviceInfoProvider(),
|
||||
this.biometricPrompt = const PlaceholderBaronSafeBiometricPrompt(),
|
||||
this.pushService = const PlaceholderBaronSafePushService(),
|
||||
});
|
||||
|
||||
final BaronSafeDeviceInfoProvider deviceInfoProvider;
|
||||
final BaronSafeBiometricPrompt biometricPrompt;
|
||||
final BaronSafePushService pushService;
|
||||
|
||||
@override
|
||||
Future<BaronSafeBridgeResponse> handle(BaronSafeBridgeRequest request) async {
|
||||
return switch (request.command) {
|
||||
BaronSafeBridgeCommand.getDeviceInfo => _getDeviceInfo(),
|
||||
BaronSafeBridgeCommand.openBiometricPrompt => _openBiometricPrompt(
|
||||
request.payload,
|
||||
),
|
||||
BaronSafeBridgeCommand.registerPushToken => _registerPushToken(),
|
||||
BaronSafeBridgeCommand.blockSession => _notImplemented(request.command),
|
||||
BaronSafeBridgeCommand.blockLinkedApp => _notImplemented(request.command),
|
||||
BaronSafeBridgeCommand.openSettings => _notImplemented(request.command),
|
||||
};
|
||||
}
|
||||
|
||||
Future<BaronSafeBridgeResponse> _getDeviceInfo() async {
|
||||
final deviceInfo = await deviceInfoProvider.readDeviceInfo();
|
||||
return BaronSafeBridgeResponse.success(data: deviceInfo.toJson());
|
||||
}
|
||||
|
||||
Future<BaronSafeBridgeResponse> _openBiometricPrompt(
|
||||
Map<String, Object?> payload,
|
||||
) async {
|
||||
final result = await biometricPrompt.verify(
|
||||
reason: payload['reason'] as String? ?? 'Verify Baron Safe action',
|
||||
);
|
||||
|
||||
return BaronSafeBridgeResponse.success(data: result.toJson());
|
||||
}
|
||||
|
||||
BaronSafeBridgeResponse _notImplemented(BaronSafeBridgeCommand command) {
|
||||
return BaronSafeBridgeResponse.failure(
|
||||
BaronSafeBridgeError(
|
||||
code: 'not_implemented',
|
||||
message: '${command.value} is not implemented yet.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<BaronSafeBridgeResponse> _registerPushToken() async {
|
||||
final result = await pushService.registerCurrentToken();
|
||||
return BaronSafeBridgeResponse.success(data: result.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceholderBaronSafeDeviceInfoProvider
|
||||
implements BaronSafeDeviceInfoProvider {
|
||||
const PlaceholderBaronSafeDeviceInfoProvider();
|
||||
|
||||
@override
|
||||
Future<BaronSafeDeviceInfo> readDeviceInfo() async {
|
||||
return const BaronSafeDeviceInfo(
|
||||
deviceId: 'placeholder-device',
|
||||
platform: 'flutter',
|
||||
appVersion: '0.1.0',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceholderBaronSafeBiometricPrompt implements BaronSafeBiometricPrompt {
|
||||
const PlaceholderBaronSafeBiometricPrompt();
|
||||
|
||||
@override
|
||||
Future<BaronSafeBiometricResult> verify({required String reason}) async {
|
||||
return BaronSafeBiometricResult(
|
||||
verified: false,
|
||||
reason: 'Biometric prompt placeholder: $reason',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../webview/baron_safe_webview_screen.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
static const routeName = 'home';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Baron Safe'), centerTitle: false),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
Text(
|
||||
'Secure Baron SSO companion',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Mobile shell is ready for WebView, native bridge, push, and secure storage wiring.',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_HomeActionTile(
|
||||
icon: Icons.shield_outlined,
|
||||
title: 'Baron Safe WebView',
|
||||
subtitle: 'Route container placeholder',
|
||||
onTap: () => context.goNamed(BaronSafeWebViewScreen.routeName),
|
||||
),
|
||||
const _HomeActionTile(
|
||||
icon: Icons.fingerprint,
|
||||
title: 'Biometric bridge',
|
||||
subtitle: 'Native authentication placeholder',
|
||||
),
|
||||
const _HomeActionTile(
|
||||
icon: Icons.notifications_active_outlined,
|
||||
title: 'Push messaging',
|
||||
subtitle: 'FCM/APNs setup placeholder',
|
||||
),
|
||||
const _HomeActionTile(
|
||||
icon: Icons.lock_outline,
|
||||
title: 'Secure storage',
|
||||
subtitle: 'Device and session storage placeholder',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeActionTile extends StatelessWidget {
|
||||
const _HomeActionTile({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Icon(icon, color: colorScheme.primary),
|
||||
title: Text(title),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
enum BaronSafePushPlatform {
|
||||
android('android'),
|
||||
ios('ios'),
|
||||
unknown('unknown');
|
||||
|
||||
const BaronSafePushPlatform(this.value);
|
||||
|
||||
final String value;
|
||||
}
|
||||
|
||||
class BaronSafePushToken {
|
||||
const BaronSafePushToken({
|
||||
required this.token,
|
||||
required this.platform,
|
||||
required this.deviceId,
|
||||
required this.appVersion,
|
||||
});
|
||||
|
||||
final String token;
|
||||
final BaronSafePushPlatform platform;
|
||||
final String deviceId;
|
||||
final String appVersion;
|
||||
|
||||
Map<String, Object?> toJson() {
|
||||
return {
|
||||
'token': token,
|
||||
'platform': platform.value,
|
||||
'deviceId': deviceId,
|
||||
'appVersion': appVersion,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BaronSafePushRegistrationResult {
|
||||
const BaronSafePushRegistrationResult({
|
||||
required this.registered,
|
||||
required this.token,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
final bool registered;
|
||||
final BaronSafePushToken? token;
|
||||
final String message;
|
||||
|
||||
Map<String, Object?> toJson() {
|
||||
return {
|
||||
'registered': registered,
|
||||
'token': token?.toJson(),
|
||||
'message': message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'baron_safe_push_models.dart';
|
||||
|
||||
abstract interface class BaronSafePushTokenProvider {
|
||||
Future<BaronSafePushToken?> readCurrentToken();
|
||||
}
|
||||
|
||||
abstract interface class BaronSafePushTokenRegistrar {
|
||||
Future<void> register(BaronSafePushToken token);
|
||||
}
|
||||
|
||||
abstract interface class BaronSafePushService {
|
||||
Future<void> initialize();
|
||||
|
||||
Future<BaronSafePushRegistrationResult> registerCurrentToken();
|
||||
}
|
||||
|
||||
class PlaceholderBaronSafePushService implements BaronSafePushService {
|
||||
const PlaceholderBaronSafePushService({
|
||||
this.tokenProvider = const PlaceholderBaronSafePushTokenProvider(),
|
||||
this.tokenRegistrar = const NoopBaronSafePushTokenRegistrar(),
|
||||
});
|
||||
|
||||
final BaronSafePushTokenProvider tokenProvider;
|
||||
final BaronSafePushTokenRegistrar tokenRegistrar;
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
// Firebase/APNs platform setup is intentionally deferred to a later step.
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BaronSafePushRegistrationResult> registerCurrentToken() async {
|
||||
final token = await tokenProvider.readCurrentToken();
|
||||
if (token == null) {
|
||||
return const BaronSafePushRegistrationResult(
|
||||
registered: false,
|
||||
token: null,
|
||||
message: 'Push token is not available yet.',
|
||||
);
|
||||
}
|
||||
|
||||
await tokenRegistrar.register(token);
|
||||
return BaronSafePushRegistrationResult(
|
||||
registered: true,
|
||||
token: token,
|
||||
message: 'Push token registration placeholder completed.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceholderBaronSafePushTokenProvider
|
||||
implements BaronSafePushTokenProvider {
|
||||
const PlaceholderBaronSafePushTokenProvider();
|
||||
|
||||
@override
|
||||
Future<BaronSafePushToken?> readCurrentToken() async {
|
||||
return const BaronSafePushToken(
|
||||
token: 'placeholder-push-token',
|
||||
platform: BaronSafePushPlatform.unknown,
|
||||
deviceId: 'placeholder-device',
|
||||
appVersion: '0.1.0',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NoopBaronSafePushTokenRegistrar implements BaronSafePushTokenRegistrar {
|
||||
const NoopBaronSafePushTokenRegistrar();
|
||||
|
||||
@override
|
||||
Future<void> register(BaronSafePushToken token) async {
|
||||
// Backend API call to POST /api/v1/baron-safe/devices/push-token is deferred.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import 'baron_safe_secure_storage_keys.dart';
|
||||
|
||||
abstract interface class BaronSafeKeyValueStore {
|
||||
Future<String?> read(BaronSafeSecureStorageKey key);
|
||||
|
||||
Future<void> write(BaronSafeSecureStorageKey key, String value);
|
||||
|
||||
Future<void> delete(BaronSafeSecureStorageKey key);
|
||||
}
|
||||
|
||||
class FlutterBaronSafeKeyValueStore implements BaronSafeKeyValueStore {
|
||||
const FlutterBaronSafeKeyValueStore({
|
||||
FlutterSecureStorage storage = const FlutterSecureStorage(),
|
||||
}) : _storage = storage;
|
||||
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
@override
|
||||
Future<String?> read(BaronSafeSecureStorageKey key) {
|
||||
return _storage.read(key: key.value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write(BaronSafeSecureStorageKey key, String value) {
|
||||
return _storage.write(key: key.value, value: value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(BaronSafeSecureStorageKey key) {
|
||||
return _storage.delete(key: key.value);
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryBaronSafeKeyValueStore implements BaronSafeKeyValueStore {
|
||||
MemoryBaronSafeKeyValueStore({Map<BaronSafeSecureStorageKey, String>? seed})
|
||||
: _values = Map<BaronSafeSecureStorageKey, String>.of(seed ?? {});
|
||||
|
||||
final Map<BaronSafeSecureStorageKey, String> _values;
|
||||
|
||||
@override
|
||||
Future<String?> read(BaronSafeSecureStorageKey key) async {
|
||||
return _values[key];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write(BaronSafeSecureStorageKey key, String value) async {
|
||||
_values[key] = value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(BaronSafeSecureStorageKey key) async {
|
||||
_values.remove(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
enum BaronSafeSecureStorageKey {
|
||||
deviceId('baron_safe.device_id'),
|
||||
appLockEnabled('baron_safe.app_lock_enabled'),
|
||||
biometricEnabled('baron_safe.biometric_enabled');
|
||||
|
||||
const BaronSafeSecureStorageKey(this.value);
|
||||
|
||||
final String value;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'baron_safe_secure_storage.dart';
|
||||
import 'baron_safe_secure_storage_keys.dart';
|
||||
|
||||
abstract interface class BaronSafeSecureStorageService {
|
||||
Future<String?> readDeviceId();
|
||||
|
||||
Future<void> saveDeviceId(String deviceId);
|
||||
|
||||
Future<bool> isAppLockEnabled();
|
||||
|
||||
Future<void> setAppLockEnabled({required bool enabled});
|
||||
|
||||
Future<bool> isBiometricEnabled();
|
||||
|
||||
Future<void> setBiometricEnabled({required bool enabled});
|
||||
|
||||
Future<void> clearDeviceBinding();
|
||||
}
|
||||
|
||||
class DefaultBaronSafeSecureStorageService
|
||||
implements BaronSafeSecureStorageService {
|
||||
const DefaultBaronSafeSecureStorageService(this._store);
|
||||
|
||||
final BaronSafeKeyValueStore _store;
|
||||
|
||||
@override
|
||||
Future<String?> readDeviceId() {
|
||||
return _store.read(BaronSafeSecureStorageKey.deviceId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveDeviceId(String deviceId) {
|
||||
return _store.write(BaronSafeSecureStorageKey.deviceId, deviceId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isAppLockEnabled() async {
|
||||
return _readBool(BaronSafeSecureStorageKey.appLockEnabled);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setAppLockEnabled({required bool enabled}) {
|
||||
return _writeBool(BaronSafeSecureStorageKey.appLockEnabled, enabled);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isBiometricEnabled() {
|
||||
return _readBool(BaronSafeSecureStorageKey.biometricEnabled);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setBiometricEnabled({required bool enabled}) {
|
||||
return _writeBool(BaronSafeSecureStorageKey.biometricEnabled, enabled);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearDeviceBinding() async {
|
||||
await _store.delete(BaronSafeSecureStorageKey.deviceId);
|
||||
}
|
||||
|
||||
Future<bool> _readBool(BaronSafeSecureStorageKey key) async {
|
||||
return await _store.read(key) == 'true';
|
||||
}
|
||||
|
||||
Future<void> _writeBool(BaronSafeSecureStorageKey key, bool value) {
|
||||
return _store.write(key, value.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
class BaronSafeWebConfig {
|
||||
const BaronSafeWebConfig._();
|
||||
|
||||
static const initialUrl = String.fromEnvironment(
|
||||
'BARON_SAFE_WEB_URL',
|
||||
defaultValue: 'https://safe.baron.hmac.kr',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import 'baron_safe_web_config.dart';
|
||||
|
||||
class BaronSafeWebViewScreen extends StatefulWidget {
|
||||
const BaronSafeWebViewScreen({super.key});
|
||||
|
||||
static const routeName = 'baron-safe-webview';
|
||||
|
||||
@override
|
||||
State<BaronSafeWebViewScreen> createState() => _BaronSafeWebViewScreenState();
|
||||
}
|
||||
|
||||
class _BaronSafeWebViewScreenState extends State<BaronSafeWebViewScreen> {
|
||||
late final WebViewController _controller;
|
||||
var _loadingProgress = 0;
|
||||
WebResourceError? _lastError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageStarted: (_) {
|
||||
setState(() {
|
||||
_loadingProgress = 0;
|
||||
_lastError = null;
|
||||
});
|
||||
},
|
||||
onProgress: (progress) {
|
||||
setState(() => _loadingProgress = progress);
|
||||
},
|
||||
onPageFinished: (_) {
|
||||
setState(() => _loadingProgress = 100);
|
||||
},
|
||||
onWebResourceError: (error) {
|
||||
setState(() => _lastError = error);
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(BaronSafeWebConfig.initialUrl));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Baron Safe'),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Reload',
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _controller.reload,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
WebViewWidget(controller: _controller),
|
||||
if (_loadingProgress < 100)
|
||||
LinearProgressIndicator(value: _loadingProgress / 100),
|
||||
if (_lastError != null)
|
||||
_WebViewErrorState(
|
||||
description: _lastError!.description,
|
||||
onRetry: _controller.reload,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WebViewErrorState extends StatelessWidget {
|
||||
const _WebViewErrorState({required this.description, required this.onRetry});
|
||||
|
||||
final String description;
|
||||
final VoidCallback onRetry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return ColoredBox(
|
||||
color: colorScheme.surface,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.wifi_off_outlined, size: 40, color: colorScheme.error),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Unable to load Baron Safe',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user