Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/dart-format-fix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@ jobs:
flutter-version: '3.41.7'
cache: true

- name: Install package dependencies
# REQUIRED before `dart format`: the formatter derives each file's
# language version from .dart_tool/package_config.json. Without it the
# formatter falls back to the pubspec lower bound (3.4) and emits the OLD
# style, while CI — which runs `pub get` first — expects the NEW one. That
# mismatch is what made this bot and CI overwrite each other forever.
# `dart format` 之前必需:格式化器依据 .dart_tool/package_config.json 决定每个
# 文件的语言版本。缺失时会回退到 pubspec 下界(3.4)并输出旧风格,而先执行了
# `pub get` 的 CI 期望新风格 —— 正是这个差异让 bot 与 CI 长期互相覆盖。
run: dart pub get

- name: Install example dependencies
working-directory: example
run: flutter pub get

- name: Install demo backend dependencies
working-directory: server
run: dart pub get

- name: Format package
run: dart format .

Expand Down
154 changes: 78 additions & 76 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ class _DemoStrategy implements AuthStrategy {

@override
Future<AuthSession> refresh(RefreshToken token) async => login(
const Credentials(username: _validUsername, password: _validPassword),
);
const Credentials(username: _validUsername, password: _validPassword),
);
}

/// Real HTTP backend strategy. Talks to the Dart server in `../../server`.
Expand All @@ -64,14 +64,14 @@ class _HttpAuthStrategy implements AuthStrategy {
res.data as Map<String, dynamic>;

AuthSession _toSession(Map<String, dynamic> data) => AuthSession(
accessToken: data['accessToken'] as String,
refreshToken: RefreshToken(data['refreshToken'] as String),
expiresAt: data['expiresIn'] != null
? DateTime.now().add(Duration(seconds: data['expiresIn'] as int))
: null,
userId: data['userId'] as String,
displayName: data['displayName'] as String,
);
accessToken: data['accessToken'] as String,
refreshToken: RefreshToken(data['refreshToken'] as String),
expiresAt: data['expiresIn'] != null
? DateTime.now().add(Duration(seconds: data['expiresIn'] as int))
: null,
userId: data['userId'] as String,
displayName: data['displayName'] as String,
);

@override
Future<AuthSession> login(Credentials credentials) async {
Expand Down Expand Up @@ -194,10 +194,10 @@ class _DemoAppState extends State<DemoApp> {
}

void _toggleBackend(bool value) => setState(() {
unawaited(_auth.dispose());
_useBackend = value;
_init();
});
unawaited(_auth.dispose());
_useBackend = value;
_init();
});

/// Runs an auth action and swallows the rethrown error: [AuthManager] already
/// surfaces it as an [AuthError] state, so there is nothing left to handle.
Expand Down Expand Up @@ -271,50 +271,51 @@ class _DemoAppState extends State<DemoApp> {

@override
Widget build(BuildContext context) => MaterialApp(
title: 'zero_auth demo',
theme: _theme(Brightness.light),
darkTheme: _theme(Brightness.dark),
home: Scaffold(
appBar: AppBar(title: const Text('zero_auth demo')),
body: SafeArea(
child: StreamBuilder<AuthState>(
initialData: _auth.current,
stream: _auth.state,
builder: (context, snapshot) => _DemoBody(
state: snapshot.data,
auth: _auth,
useBackend: _useBackend,
baseUrl: _baseUrl,
username: _username,
password: _password,
onToggleBackend: _toggleBackend,
onLogin: () => _invoke(
() => _auth.login(
Credentials(username: _username.text, password: _password.text),
title: 'zero_auth demo',
theme: _theme(Brightness.light),
darkTheme: _theme(Brightness.dark),
home: Scaffold(
appBar: AppBar(title: const Text('zero_auth demo')),
body: SafeArea(
child: StreamBuilder<AuthState>(
initialData: _auth.current,
stream: _auth.state,
builder: (context, snapshot) => _DemoBody(
state: snapshot.data,
auth: _auth,
useBackend: _useBackend,
baseUrl: _baseUrl,
username: _username,
password: _password,
onToggleBackend: _toggleBackend,
onLogin: () => _invoke(
() => _auth.login(
Credentials(
username: _username.text, password: _password.text),
),
),
onRefresh: () => _invoke(() => _auth.refresh()),
onLogout: () => _invoke(() => _auth.logout()),
onCallMe: () => _callMe(context),
onExpireNow: () => _expireTokenNow(context),
onExpireSoon: () => _expireTokenSoon(context, 10),
onResetDebug: () => _resetDebug(context),
),
),
onRefresh: () => _invoke(() => _auth.refresh()),
onLogout: () => _invoke(() => _auth.logout()),
onCallMe: () => _callMe(context),
onExpireNow: () => _expireTokenNow(context),
onExpireSoon: () => _expireTokenSoon(context, 10),
onResetDebug: () => _resetDebug(context),
),
),
),
),
);
);

/// One seed colour drives the whole palette; widgets read shades from the
/// theme instead of hardcoding colors.
static ThemeData _theme(Brightness brightness) => ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF00695C),
brightness: brightness,
),
);
useMaterial3: true,
brightness: brightness,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF00695C),
brightness: brightness,
),
);
}

/// The scrollable demo surface. Adapts to the viewport: a full-width column on
Expand Down Expand Up @@ -578,16 +579,17 @@ class _Badge extends StatelessWidget {

@override
Widget build(BuildContext context) => Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color,
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: Text(
label,
style: Theme.of(context).textTheme.labelMedium?.copyWith(color: onColor),
),
);
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color,
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: Text(
label,
style:
Theme.of(context).textTheme.labelMedium?.copyWith(color: onColor),
),
);
}

/// The complete access token, wrapped over as many lines as it needs.
Expand Down Expand Up @@ -679,13 +681,13 @@ class _InfoRow extends StatelessWidget {

@override
Widget build(BuildContext context) => _LabeledRow(
label: label,
child: Text(
value,
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
);
label: label,
child: Text(
value,
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
);
}

/// Failure surface. Uses the theme's error container rather than literal red,
Expand Down Expand Up @@ -751,15 +753,15 @@ class _BackendCard extends StatelessWidget {

@override
Widget build(BuildContext context) => Card(
child: SwitchListTile.adaptive(
value: useBackend,
onChanged: onChanged,
title: const Text('Live backend'),
subtitle: Text(useBackend ? baseUrl : 'offline double, no server'),
secondary: const Icon(Icons.cloud_outlined),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
);
child: SwitchListTile.adaptive(
value: useBackend,
onChanged: onChanged,
title: const Text('Live backend'),
subtitle: Text(useBackend ? baseUrl : 'offline double, no server'),
secondary: const Icon(Icons.cloud_outlined),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
);
}

/// Sign-in form. The password can be revealed, and submitting from the keyboard
Expand Down
2 changes: 1 addition & 1 deletion example/lib/secure_token_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:zero_auth/zero_auth.dart';
/// is required. Swap for your own codec as needed.
final class SecureTokenStore implements TokenStore {
SecureTokenStore([FlutterSecureStorage? storage])
: _storage = storage ?? const FlutterSecureStorage();
: _storage = storage ?? const FlutterSecureStorage();

final FlutterSecureStorage _storage;

Expand Down
40 changes: 20 additions & 20 deletions lib/src/auth_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,17 @@ final class AuthManager implements AuthTokenSource {
Duration? clockSkew,
this.preserveSessionDetails = true,
this.onStateChanged,
}) : tokenStore = tokenStore ?? InMemoryTokenStore(),
_autoRefreshAhead = autoRefreshAhead,
_autoRefreshRetryDelay =
autoRefreshRetryDelay ?? const Duration(seconds: 30),
_autoRefreshMaxRetries = autoRefreshMaxRetries ?? 3,
_autoRefreshMinInterval =
autoRefreshMinInterval ?? const Duration(seconds: 5),
refreshFailurePolicy =
refreshFailurePolicy ?? defaultRefreshFailurePolicy,
clock = clock ?? _systemClock,
clockSkew = clockSkew ?? const Duration(seconds: 30);
}) : tokenStore = tokenStore ?? InMemoryTokenStore(),
_autoRefreshAhead = autoRefreshAhead,
_autoRefreshRetryDelay =
autoRefreshRetryDelay ?? const Duration(seconds: 30),
_autoRefreshMaxRetries = autoRefreshMaxRetries ?? 3,
_autoRefreshMinInterval =
autoRefreshMinInterval ?? const Duration(seconds: 5),
refreshFailurePolicy =
refreshFailurePolicy ?? defaultRefreshFailurePolicy,
clock = clock ?? _systemClock,
clockSkew = clockSkew ?? const Duration(seconds: 30);

final Duration? _autoRefreshAhead;

Expand Down Expand Up @@ -220,10 +220,10 @@ final class AuthManager implements AuthTokenSource {
/// 当前活动会话;未认证时为 `null`。在 [Authenticated] 与 [Refreshing] 下均可用
/// (续期中会话依然有效),但 [LoggingOut] 下为空。
AuthSession? get currentSession => switch (_state) {
Authenticated(:final session) => session,
Refreshing(:final session) => session,
_ => null,
};
Authenticated(:final session) => session,
Refreshing(:final session) => session,
_ => null,
};

@override
String? get accessToken => currentSession?.accessToken;
Expand Down Expand Up @@ -647,11 +647,11 @@ final class AuthManager implements AuthTokenSource {
/// being discarded during [LoggingOut].
/// 驱动进行中操作或已建立认证的会话,包含在 [LoggingOut] 期间正被丢弃的那个。
AuthSession? get _activeSession => switch (_state) {
Authenticated(:final session) => session,
Refreshing(:final session) => session,
LoggingOut(:final session) => session,
_ => null,
};
Authenticated(:final session) => session,
Refreshing(:final session) => session,
LoggingOut(:final session) => session,
_ => null,
};

bool _isCurrent(int epoch) => !_disposed && epoch == _epoch;

Expand Down
12 changes: 5 additions & 7 deletions lib/src/auth_manager_group.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ final class AuthManagerGroup implements AuthTokenSource {
this.clockSkew,
this.preserveSessionDetails = true,
this.onStateChanged,
}) : _strategyFactory = strategyFactory,
_storeFactory = storeFactory;
}) : _strategyFactory = strategyFactory,
_storeFactory = storeFactory;

/// Called once per account. Returning the same instance for every account is
/// fine — and typical — since a strategy usually just talks to one backend.
Expand All @@ -79,8 +79,7 @@ final class AuthManagerGroup implements AuthTokenSource {
String accountId,
AuthStrategy strategy,
TokenStore store,
)?
managerFactory;
)? managerFactory;

/// Forwarded to every manager this group creates — see [AuthManager.new].
/// 转发给分组创建的每个管理器 —— 参见 [AuthManager.new]。
Expand Down Expand Up @@ -157,9 +156,8 @@ final class AuthManagerGroup implements AuthTokenSource {
clock: clock,
clockSkew: clockSkew,
preserveSessionDetails: preserveSessionDetails,
onStateChanged: observer == null
? null
: (state) => observer(accountId, state),
onStateChanged:
observer == null ? null : (state) => observer(accountId, state),
);
}

Expand Down
Loading
Loading