Expand Flutter app scaffold
ci / flutter-check (push) Successful in 4s

This commit is contained in:
2026-06-30 16:21:45 +09:00
parent 684cf87702
commit d2f6af7657
84 changed files with 3342 additions and 28 deletions
+2 -27
View File
@@ -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'),
),
);
}
}
+47
View File
@@ -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'),
),
],
),
),
),
);
}
}