1
0
forked from baron/baron-sso

로그인 화면 플랫 UI 수정

This commit is contained in:
2026-04-08 17:45:51 +09:00
parent 332b657add
commit 1e53b66abb
6 changed files with 730 additions and 553 deletions

View File

@@ -2,10 +2,15 @@ import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class ThemeController extends ValueNotifier<ThemeMode> { class ThemeController extends ValueNotifier<ThemeMode> {
ThemeController._() : super(ThemeMode.light); ThemeController._(this.storageKey) : super(ThemeMode.light);
static const storageKey = 'userfront_theme'; static const appStorageKey = 'userfront_theme';
static final ThemeController instance = ThemeController._(); static const authStorageKey = 'userfront_auth_theme';
static final ThemeController app = ThemeController._(appStorageKey);
static final ThemeController auth = ThemeController._(authStorageKey);
static final ThemeController instance = app;
final String storageKey;
bool get isDark => value == ThemeMode.dark; bool get isDark => value == ThemeMode.dark;

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'app_theme.dart';
import 'theme_controller.dart';
class ThemeScope extends InheritedWidget {
const ThemeScope({super.key, required this.controller, required Widget child})
: super(child: child);
final ThemeController controller;
static ThemeController of(BuildContext context) {
final scope = context.dependOnInheritedWidgetOfExactType<ThemeScope>();
return scope?.controller ?? ThemeController.app;
}
@override
bool updateShouldNotify(ThemeScope oldWidget) {
return oldWidget.controller != controller;
}
}
class ScopedTheme extends StatelessWidget {
const ScopedTheme({super.key, required this.controller, required this.child});
final ThemeController controller;
final Widget child;
@override
Widget build(BuildContext context) {
return ThemeScope(
controller: controller,
child: ValueListenableBuilder<ThemeMode>(
valueListenable: controller,
builder: (context, mode, _) {
return Theme(
data: mode == ThemeMode.dark ? buildDarkTheme() : buildLightTheme(),
child: child,
);
},
),
);
}
}

View File

@@ -1,8 +1,7 @@
import 'package:easy_localization/easy_localization.dart' hide tr;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:userfront/i18n.dart'; import 'package:userfront/i18n.dart';
import '../theme/theme_controller.dart'; import '../theme/theme_scope.dart';
class ThemeToggleButton extends StatelessWidget { class ThemeToggleButton extends StatelessWidget {
const ThemeToggleButton({super.key, this.compact = false}); const ThemeToggleButton({super.key, this.compact = false});
@@ -11,10 +10,11 @@ class ThemeToggleButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
context.locale; Localizations.localeOf(context);
final controller = ThemeScope.of(context);
return ValueListenableBuilder<ThemeMode>( return ValueListenableBuilder<ThemeMode>(
valueListenable: ThemeController.instance, valueListenable: controller,
builder: (context, mode, _) { builder: (context, mode, _) {
final isLight = mode == ThemeMode.light; final isLight = mode == ThemeMode.light;
final icon = isLight final icon = isLight
@@ -28,13 +28,13 @@ class ThemeToggleButton extends StatelessWidget {
if (compact) { if (compact) {
return IconButton( return IconButton(
tooltip: tooltip, tooltip: tooltip,
onPressed: () => ThemeController.instance.toggle(), onPressed: () => controller.toggle(),
icon: Icon(icon), icon: Icon(icon),
); );
} }
return OutlinedButton.icon( return OutlinedButton.icon(
onPressed: () => ThemeController.instance.toggle(), onPressed: () => controller.toggle(),
icon: Icon(icon, size: 18), icon: Icon(icon, size: 18),
label: Text(label), label: Text(label),
); );

View File

@@ -1389,6 +1389,73 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
final mutedColor = colorScheme.onSurfaceVariant; final mutedColor = colorScheme.onSurfaceVariant;
final inputForegroundColor = colorScheme.brightness == Brightness.dark
? const Color(0xFFE2E8F0)
: const Color(0xFF334155);
final primaryColor = colorScheme.brightness == Brightness.dark
? const Color(0xFF93C5FD)
: const Color(0xFF1E3A8A);
final onPrimaryColor = colorScheme.brightness == Brightness.dark
? const Color(0xFF0F172A)
: Colors.white;
final inputDecorationTheme = theme.inputDecorationTheme.copyWith(
filled: false,
fillColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 18),
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: primaryColor, width: 1.6),
),
labelStyle: TextStyle(color: inputForegroundColor),
floatingLabelStyle: TextStyle(color: primaryColor),
hintStyle: TextStyle(color: inputForegroundColor),
prefixIconColor: inputForegroundColor,
);
final localTheme = theme.copyWith(
inputDecorationTheme: inputDecorationTheme,
tabBarTheme: theme.tabBarTheme.copyWith(
dividerColor: colorScheme.outlineVariant,
indicatorColor: primaryColor,
labelColor: colorScheme.onSurface,
unselectedLabelColor: mutedColor,
labelStyle: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
unselectedLabelStyle: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(48),
backgroundColor: primaryColor,
foregroundColor: onPrimaryColor,
textStyle: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: primaryColor,
textStyle: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
);
if (_verificationOnly && _verificationApproved) { if (_verificationOnly && _verificationApproved) {
return Scaffold( return Scaffold(
@@ -1408,16 +1475,18 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
backgroundColor: colorScheme.surfaceContainerLowest, backgroundColor: colorScheme.surfaceContainerLowest,
body: LayoutBuilder( body: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return SingleChildScrollView( return Theme(
data: localTheme,
child: SingleChildScrollView(
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight), constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Center( child: Center(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 400), constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.all(24), padding: const EdgeInsets.symmetric(
child: Card( horizontal: 28,
child: Padding( vertical: 40,
padding: const EdgeInsets.all(24), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -1425,20 +1494,22 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Text( Text(
tr('ui.userfront.app_title'), tr('ui.userfront.app_title'),
style: theme.textTheme.headlineMedium?.copyWith( style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold, fontSize: 34,
fontWeight: FontWeight.w800,
letterSpacing: -0.7,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
if (_drySendEnabled) ...[ if (_drySendEnabled) ...[
const SizedBox(height: 16), const SizedBox(height: 20),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 14,
vertical: 10, vertical: 12,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFFFF3CD), color: const Color(0xFFFFF3CD),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: const Color(0xFFFFC107), color: const Color(0xFFFFC107),
), ),
@@ -1449,7 +1520,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Icons.warning_amber_rounded, Icons.warning_amber_rounded,
color: Color(0xFF8A6D3B), color: Color(0xFF8A6D3B),
), ),
const SizedBox(width: 8), const SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
tr('msg.userfront.login.dry_send'), tr('msg.userfront.login.dry_send'),
@@ -1463,32 +1534,41 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
), ),
), ),
], ],
const SizedBox(height: 40), const SizedBox(height: 52),
Padding(
TabBar( padding: const EdgeInsets.symmetric(horizontal: 34),
child: TabBar(
controller: _tabController, controller: _tabController,
indicatorSize: TabBarIndicatorSize.label,
tabs: [ tabs: [
Tab(text: tr('ui.userfront.login.tabs.password')), Tab(text: tr('ui.userfront.login.tabs.password')),
Tab(text: tr('ui.userfront.login.tabs.link')), Tab(text: tr('ui.userfront.login.tabs.link')),
Tab(text: tr('ui.userfront.login.tabs.qr')), Tab(text: tr('ui.userfront.login.tabs.qr')),
], ],
), ),
const SizedBox(height: 24), ),
const SizedBox(height: 28),
SizedBox( SizedBox(
height: 350, height: 360,
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(top: 16.0), padding: const EdgeInsets.only(top: 20),
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 356,
),
child: Column( child: Column(
children: [ children: [
TextField( TextField(
key: const ValueKey( key: const ValueKey(
'password_login_id_input', 'password_login_id_input',
), ),
controller: _passwordLoginIdController, controller:
_passwordLoginIdController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: labelText:
_loginIdLabel ?? _loginIdLabel ??
@@ -1497,12 +1577,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
), ),
prefixIcon: const Icon( prefixIcon: const Icon(
Icons.person_outline, Icons.person_outline,
size: 22,
), ),
), ),
onSubmitted: (_) => onSubmitted: (_) =>
_handlePasswordLogin(), _handlePasswordLogin(),
), ),
const SizedBox(height: 16), const SizedBox(height: 18),
TextField( TextField(
key: const ValueKey( key: const ValueKey(
'password_login_password_input', 'password_login_password_input',
@@ -1516,13 +1597,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
), ),
prefixIcon: const Icon( prefixIcon: const Icon(
Icons.lock_outline, Icons.lock_outline,
size: 22,
), ),
), ),
onSubmitted: (_) => onSubmitted: (_) =>
_handlePasswordLogin(), _handlePasswordLogin(),
), ),
if (_isPasswordCapsLockOn) ...[ if (_isPasswordCapsLockOn) ...[
const SizedBox(height: 8), const SizedBox(height: 10),
Row( Row(
children: [ children: [
const Icon( const Icon(
@@ -1544,17 +1626,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
], ],
), ),
], ],
const SizedBox(height: 24), const SizedBox(height: 28),
FilledButton( FilledButton(
key: const ValueKey( key: const ValueKey(
'password_login_submit_button', 'password_login_submit_button',
), ),
onPressed: _handlePasswordLogin, onPressed: _handlePasswordLogin,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(
50,
),
),
child: Text( child: Text(
tr( tr(
'ui.userfront.login.action.submit', 'ui.userfront.login.action.submit',
@@ -1564,9 +1641,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
], ],
), ),
), ),
),
),
Padding( Padding(
padding: const EdgeInsets.only(top: 16.0), padding: const EdgeInsets.only(top: 20),
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 356,
),
child: Column( child: Column(
children: [ children: [
if (_linkPendingRef == null) ...[ if (_linkPendingRef == null) ...[
@@ -1579,29 +1663,30 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
hintText: '', hintText: '',
prefixIcon: const Icon( prefixIcon: const Icon(
Icons.person_outline, Icons.person_outline,
size: 22,
), ),
), ),
onSubmitted: (_) => onSubmitted: (_) =>
_handleLinkLogin(), _handleLinkLogin(),
), ),
const SizedBox(height: 24), const SizedBox(height: 28),
FilledButton( FilledButton(
onPressed: _handleLinkLogin, onPressed: _handleLinkLogin,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(
50,
),
),
child: Text( child: Text(
tr('ui.userfront.login.link.send'), tr(
'ui.userfront.login.link.send',
),
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
tr('msg.userfront.login.link.helper'), tr(
'msg.userfront.login.link.helper',
),
style: TextStyle( style: TextStyle(
color: mutedColor, color: mutedColor,
fontSize: 12, fontSize: 12,
height: 1.5,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -1618,15 +1703,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
fontSize: 12, fontSize: 12,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 14),
FilledButton( FilledButton(
onPressed: () { onPressed: () {
setState(_resetLinkLoginState); setState(_resetLinkLoginState);
}, },
style: FilledButton.styleFrom(
minimumSize:
const Size.fromHeight(45),
),
child: Text( child: Text(
tr('ui.common.refresh'), tr('ui.common.refresh'),
), ),
@@ -1639,10 +1720,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
style: TextStyle( style: TextStyle(
color: mutedColor, color: mutedColor,
fontSize: 12, fontSize: 12,
height: 1.5,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 12), const SizedBox(height: 14),
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -1661,11 +1743,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
hintStyle: TextStyle( hintStyle: TextStyle(
color: mutedColor, color: mutedColor,
), ),
counterText: '',
), ),
maxLength: 2, maxLength: 2,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 10),
Expanded( Expanded(
flex: 4, flex: 4,
child: TextField( child: TextField(
@@ -1681,6 +1764,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
hintStyle: TextStyle( hintStyle: TextStyle(
color: mutedColor, color: mutedColor,
), ),
counterText: '',
suffixText: suffixText:
_linkExpireSeconds > 0 _linkExpireSeconds > 0
? tr( ? tr(
@@ -1698,7 +1782,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 14),
FilledButton( FilledButton(
onPressed: () { onPressed: () {
final prefix = final prefix =
@@ -1719,19 +1803,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
); );
return; return;
} }
_verifyShortCode(prefix + digits); _verifyShortCode(
prefix + digits,
);
}, },
style: FilledButton.styleFrom(
minimumSize:
const Size.fromHeight(45),
),
child: Text( child: Text(
tr( tr(
'ui.userfront.login.short_code.submit', 'ui.userfront.login.short_code.submit',
), ),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 14),
TextButton( TextButton(
onPressed: () { onPressed: () {
if (_linkResendSeconds > 0) { if (_linkResendSeconds > 0) {
@@ -1749,7 +1831,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
} }
final loginId = final loginId =
_lastLinkLoginId ?? _lastLinkLoginId ??
_linkIdController.text.trim(); _linkIdController.text
.trim();
if (loginId.isEmpty) { if (loginId.isEmpty) {
_showError( _showError(
tr( tr(
@@ -1831,7 +1914,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
], ],
), ),
), ),
),
),
Column( Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -1842,25 +1926,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Column( Column(
children: [ children: [
Text( Text(
tr( tr('msg.userfront.login.qr_expired'),
'msg.userfront.login.qr_expired',
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: mutedColor, color: mutedColor,
fontSize: 12, fontSize: 12,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 14),
FilledButton( FilledButton(
onPressed: _startQrFlow, onPressed: _startQrFlow,
style: FilledButton.styleFrom( child: Text(tr('ui.common.refresh')),
minimumSize:
const Size.fromHeight(45),
),
child: Text(
tr('ui.common.refresh'),
),
), ),
], ],
) )
@@ -1870,13 +1946,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
CrossAxisAlignment.center, CrossAxisAlignment.center,
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: colorScheme.outline, color: colorScheme.outline,
), ),
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(12), 18,
),
), ),
child: QrImageView( child: QrImageView(
data: _qrImageBase64!, data: _qrImageBase64!,
@@ -1885,7 +1962,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
backgroundColor: Colors.white, backgroundColor: Colors.white,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 14),
Text( Text(
_qrRemainingSeconds > 0 _qrRemainingSeconds > 0
? tr( ? tr(
@@ -1902,7 +1979,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: _qrRemainingSeconds > 30 color: _qrRemainingSeconds > 30
? Colors.blue ? primaryColor
: Colors.red, : Colors.red,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -1916,23 +1993,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
style: TextStyle( style: TextStyle(
color: mutedColor, color: mutedColor,
fontSize: 12, fontSize: 12,
height: 1.5,
), ),
), ),
TextButton( TextButton(
onPressed: _startQrFlow, onPressed: _startQrFlow,
child: Text( child: Text(
tr( tr('ui.userfront.login.qr.refresh'),
'ui.userfront.login.qr.refresh',
),
), ),
), ),
], ],
) )
else else
Text( Text(
tr( tr('msg.userfront.login.qr.load_failed'),
'msg.userfront.login.qr.load_failed',
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
@@ -1940,12 +2014,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
], ],
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 18),
Column( Column(
children: [ children: [
TextButton( TextButton(
onPressed: () => onPressed: () => context.push('/forgot-password'),
context.push('/forgot-password'),
child: Text( child: Text(
tr('ui.userfront.login.forgot_password'), tr('ui.userfront.login.forgot_password'),
), ),
@@ -1962,15 +2035,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
), ),
TextButton( TextButton(
onPressed: () => context.push('/signup'), onPressed: () => context.push('/signup'),
child: Text( child: Text(tr('ui.userfront.login.signup')),
tr('ui.userfront.login.signup'),
),
), ),
], ],
), ),
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 12),
const Wrap( const Wrap(
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
spacing: 10, spacing: 10,
@@ -1983,7 +2054,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
), ),
), ),
), ),
),
); );
}, },
), ),

View File

@@ -26,6 +26,7 @@ import 'core/services/web_window.dart';
import 'core/notifiers/auth_notifier.dart'; import 'core/notifiers/auth_notifier.dart';
import 'core/theme/app_theme.dart'; import 'core/theme/app_theme.dart';
import 'core/theme/theme_controller.dart'; import 'core/theme/theme_controller.dart';
import 'core/theme/theme_scope.dart';
import 'core/i18n/locale_gate.dart'; import 'core/i18n/locale_gate.dart';
import 'core/i18n/locale_registry.dart'; import 'core/i18n/locale_registry.dart';
import 'core/i18n/locale_utils.dart'; import 'core/i18n/locale_utils.dart';
@@ -108,7 +109,8 @@ void main() async {
// 0. Initialize Logger // 0. Initialize Logger
LoggerService.init(); LoggerService.init();
await ThemeController.instance.restore(); await ThemeController.app.restore();
await ThemeController.auth.restore();
// 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화 // 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화
await _loadBundledFonts(); await _loadBundledFonts();
@@ -180,12 +182,18 @@ final _router = GoRouter(
GoRoute( GoRoute(
path: 'dashboard', path: 'dashboard',
builder: (context, state) { builder: (context, state) {
return const DashboardScreen(); return ScopedTheme(
controller: ThemeController.app,
child: const DashboardScreen(),
);
}, },
), ),
GoRoute( GoRoute(
path: 'profile', path: 'profile',
builder: (context, state) => const ProfilePage(), builder: (context, state) => ScopedTheme(
controller: ThemeController.app,
child: const ProfilePage(),
),
), ),
GoRoute( GoRoute(
path: 'signin', path: 'signin',
@@ -195,10 +203,13 @@ final _router = GoRouter(
final redirectUrl = final redirectUrl =
state.uri.queryParameters['redirect_uri'] ?? state.uri.queryParameters['redirect_uri'] ??
state.uri.queryParameters['redirect_url']; state.uri.queryParameters['redirect_url'];
return LoginScreen( return ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(
key: state.pageKey, key: state.pageKey,
loginChallenge: loginChallenge, loginChallenge: loginChallenge,
redirectUrl: redirectUrl, redirectUrl: redirectUrl,
),
); );
}, },
), ),
@@ -211,10 +222,13 @@ final _router = GoRouter(
final redirectUrl = final redirectUrl =
state.uri.queryParameters['redirect_uri'] ?? state.uri.queryParameters['redirect_uri'] ??
state.uri.queryParameters['redirect_url']; state.uri.queryParameters['redirect_url'];
return LoginScreen( return ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(
key: state.pageKey, key: state.pageKey,
loginChallenge: loginChallenge, loginChallenge: loginChallenge,
redirectUrl: redirectUrl, redirectUrl: redirectUrl,
),
); );
}, },
), ),
@@ -230,88 +244,137 @@ final _router = GoRouter(
), ),
); );
} }
return ConsentScreen(consentChallenge: consentChallenge); return ScopedTheme(
controller: ThemeController.auth,
child: ConsentScreen(consentChallenge: consentChallenge),
);
}, },
), ),
GoRoute( GoRoute(
path: 'signup', path: 'signup',
builder: (context, state) => const SignupScreen(), builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const SignupScreen(),
),
), ),
GoRoute( GoRoute(
path: 'registration', path: 'registration',
builder: (context, state) => const SignupScreen(), builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const SignupScreen(),
),
), ),
GoRoute( GoRoute(
path: 'verify', path: 'verify',
builder: (context, state) => LoginScreen(key: state.pageKey), builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(key: state.pageKey),
),
), ),
GoRoute( GoRoute(
path: 'verify/:token', path: 'verify/:token',
builder: (context, state) { builder: (context, state) {
final token = state.pathParameters['token']; final token = state.pathParameters['token'];
return LoginScreen( return ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(
key: state.pageKey, key: state.pageKey,
verificationToken: token, verificationToken: token,
),
); );
}, },
), ),
GoRoute( GoRoute(
path: 'verification', path: 'verification',
builder: (context, state) => LoginScreen(key: state.pageKey), builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(key: state.pageKey),
),
), ),
GoRoute( GoRoute(
path: 'l/:shortCode', path: 'l/:shortCode',
builder: (context, state) { builder: (context, state) {
return LoginScreen(key: state.pageKey); return ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(key: state.pageKey),
);
}, },
), ),
GoRoute( GoRoute(
path: 'forgot-password', path: 'forgot-password',
builder: (context, state) => const ForgotPasswordScreen(), builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const ForgotPasswordScreen(),
),
), ),
GoRoute( GoRoute(
path: 'recovery', path: 'recovery',
builder: (context, state) => const ForgotPasswordScreen(), builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const ForgotPasswordScreen(),
),
), ),
GoRoute( GoRoute(
path: 'reset-password', path: 'reset-password',
builder: (context, state) => const ResetPasswordScreen(), builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const ResetPasswordScreen(),
),
), ),
GoRoute( GoRoute(
path: 'error', path: 'error',
builder: (context, state) { builder: (context, state) {
final params = state.uri.queryParameters; final params = state.uri.queryParameters;
return ErrorScreen( return ScopedTheme(
controller: ThemeController.auth,
child: ErrorScreen(
errorId: params['id'], errorId: params['id'],
errorCode: params['error'], errorCode: params['error'],
description: params['error_description'] ?? params['message'], description:
params['error_description'] ?? params['message'],
),
); );
}, },
), ),
GoRoute( GoRoute(
path: 'settings', path: 'settings',
builder: (context, state) => ErrorScreen( builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: ErrorScreen(
errorCode: 'settings_disabled', errorCode: 'settings_disabled',
description: tr('msg.userfront.settings.disabled'), description: tr('msg.userfront.settings.disabled'),
), ),
), ),
),
GoRoute( GoRoute(
path: 'approve', path: 'approve',
builder: (context, state) => builder: (context, state) => ScopedTheme(
ApproveQrScreen(pendingRef: state.uri.queryParameters['ref']), controller: ThemeController.auth,
child: ApproveQrScreen(
pendingRef: state.uri.queryParameters['ref'],
),
),
), ),
GoRoute( GoRoute(
path: 'ql/:ref', path: 'ql/:ref',
builder: (context, state) => builder: (context, state) => ScopedTheme(
ApproveQrScreen(pendingRef: state.pathParameters['ref']), controller: ThemeController.auth,
child: ApproveQrScreen(pendingRef: state.pathParameters['ref']),
),
), ),
GoRoute( GoRoute(
path: 'scan', path: 'scan',
builder: (context, state) => const QRScanScreen(), builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const QRScanScreen(),
),
), ),
GoRoute( GoRoute(
path: 'admin/users', path: 'admin/users',
builder: (context, state) => const UserManagementScreen(), builder: (context, state) => ScopedTheme(
controller: ThemeController.app,
child: const UserManagementScreen(),
),
), ),
], ],
), ),
@@ -369,9 +432,6 @@ class BaronSSOApp extends StatelessWidget {
final locale = final locale =
localization?.currentLocale ?? Locale(resolvePreferredLocaleCode()); localization?.currentLocale ?? Locale(resolvePreferredLocaleCode());
return ValueListenableBuilder<ThemeMode>(
valueListenable: ThemeController.instance,
builder: (context, themeMode, _) {
return MaterialApp.router( return MaterialApp.router(
title: tr('ui.userfront.app_title'), title: tr('ui.userfront.app_title'),
localizationsDelegates: delegates, localizationsDelegates: delegates,
@@ -384,10 +444,8 @@ class BaronSSOApp extends StatelessWidget {
}, },
theme: buildLightTheme(), theme: buildLightTheme(),
darkTheme: buildDarkTheme(), darkTheme: buildDarkTheme(),
themeMode: themeMode, themeMode: ThemeMode.light,
routerConfig: _router, routerConfig: _router,
); );
},
);
} }
} }

View File

@@ -8,25 +8,25 @@ void main() {
setUp(() async { setUp(() async {
SharedPreferences.setMockInitialValues({}); SharedPreferences.setMockInitialValues({});
await ThemeController.instance.setThemeMode(ThemeMode.light); await ThemeController.app.setThemeMode(ThemeMode.light);
}); });
test('저장된 dark 값을 복원한다', () async { test('저장된 dark 값을 복원한다', () async {
SharedPreferences.setMockInitialValues({ SharedPreferences.setMockInitialValues({
ThemeController.storageKey: 'dark', ThemeController.appStorageKey: 'dark',
}); });
await ThemeController.instance.restore(); await ThemeController.app.restore();
expect(ThemeController.instance.value, ThemeMode.dark); expect(ThemeController.app.value, ThemeMode.dark);
}); });
test('toggle 결과를 저장한다', () async { test('toggle 결과를 저장한다', () async {
await ThemeController.instance.restore(); await ThemeController.app.restore();
await ThemeController.instance.toggle(); await ThemeController.app.toggle();
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
expect(ThemeController.instance.value, ThemeMode.dark); expect(ThemeController.app.value, ThemeMode.dark);
expect(prefs.getString(ThemeController.storageKey), 'dark'); expect(prefs.getString(ThemeController.appStorageKey), 'dark');
}); });
} }