Files
minstrel/flutter_client/lib/auth/auth_provider.dart
T
bvandeusen 89d8b4b5a0 feat(flutter): send timezone on login + app-start + weekly
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>
2026-05-13 12:16:52 -04:00

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);