89d8b4b5a0
Flutter client posts FlutterTimezone.getLocalTimezone() to PUT /api/me/timezone on every setSession (login / register success) and on every AuthController.build (app cold-start with valid session), when the locally-stored tz_last_sent_at is >7 days old. Cadence tracked in flutter_secure_storage so it survives app restarts. Failures swallowed: the server's UTC default + last-known value keep the scheduler functioning until the next attempt. Completes the client side of #392 Half B (per-user timezone scheduling). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
3.3 KiB
Dart
90 lines
3.3 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:flutter_timezone/flutter_timezone.dart';
|
|
|
|
import '../api/endpoints/me.dart';
|
|
import '../library/library_providers.dart' show dioProvider;
|
|
import '../models/user.dart';
|
|
|
|
const _kServerUrl = 'server_url';
|
|
const _kSessionToken = 'session_token';
|
|
const _kCurrentUser = 'current_user';
|
|
const _kTzLastSentAt = 'tz_last_sent_at';
|
|
const _weeklyMs = 7 * 24 * 60 * 60 * 1000;
|
|
|
|
final secureStorageProvider = Provider<FlutterSecureStorage>(
|
|
(ref) => const FlutterSecureStorage(),
|
|
);
|
|
|
|
final serverUrlProvider = FutureProvider<String?>((ref) async {
|
|
return ref.watch(secureStorageProvider).read(key: _kServerUrl);
|
|
});
|
|
|
|
final sessionTokenProvider = FutureProvider<String?>((ref) async {
|
|
return ref.watch(secureStorageProvider).read(key: _kSessionToken);
|
|
});
|
|
|
|
class AuthController extends AsyncNotifier<User?> {
|
|
late FlutterSecureStorage _storage;
|
|
|
|
@override
|
|
Future<User?> build() async {
|
|
_storage = ref.watch(secureStorageProvider);
|
|
final raw = await _storage.read(key: _kCurrentUser);
|
|
if (raw == null) return null;
|
|
final user = User.fromJson(jsonDecode(raw) as Map<String, dynamic>);
|
|
// Fire-and-forget timezone send on app start with an existing
|
|
// session — no-op when last_sent_at is fresh.
|
|
// ignore: unawaited_futures
|
|
_sendTimezoneIfStale();
|
|
return user;
|
|
}
|
|
|
|
Future<void> setServerUrl(String url) async {
|
|
await _storage.write(key: _kServerUrl, value: url);
|
|
ref.invalidate(serverUrlProvider);
|
|
}
|
|
|
|
Future<void> setSession({required String token, required String userJson}) async {
|
|
await _storage.write(key: _kSessionToken, value: token);
|
|
await _storage.write(key: _kCurrentUser, value: userJson);
|
|
ref.invalidate(sessionTokenProvider);
|
|
state = AsyncData(User.fromJson(jsonDecode(userJson) as Map<String, dynamic>));
|
|
// ignore: unawaited_futures
|
|
_sendTimezoneIfStale();
|
|
}
|
|
|
|
Future<void> clearSession() async {
|
|
await _storage.delete(key: _kSessionToken);
|
|
await _storage.delete(key: _kCurrentUser);
|
|
ref.invalidate(sessionTokenProvider);
|
|
state = const AsyncData(null);
|
|
}
|
|
|
|
/// Sends the device's current IANA timezone to PUT /api/me/timezone
|
|
/// when the last successful send was >7 days ago (or never).
|
|
/// Cadence persists in flutter_secure_storage so it survives app
|
|
/// restarts. Failures are swallowed: the server keeps its previous
|
|
/// value (or 'UTC' default) until the next attempt.
|
|
Future<void> _sendTimezoneIfStale() async {
|
|
try {
|
|
final lastStr = await _storage.read(key: _kTzLastSentAt);
|
|
final lastMs = lastStr == null ? 0 : int.tryParse(lastStr) ?? 0;
|
|
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
|
if (nowMs - lastMs < _weeklyMs) return;
|
|
final tz = await FlutterTimezone.getLocalTimezone();
|
|
if (tz.isEmpty) return;
|
|
final dio = await ref.read(dioProvider.future);
|
|
await MeApi(dio).putTimezone(tz);
|
|
await _storage.write(key: _kTzLastSentAt, value: nowMs.toString());
|
|
} catch (_) {
|
|
// Non-fatal — server falls back to UTC or last-known value.
|
|
}
|
|
}
|
|
}
|
|
|
|
final authControllerProvider =
|
|
AsyncNotifierProvider<AuthController, User?>(AuthController.new);
|