mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-11 20:34:04 +01:00
phone_app: the alarm now syncs through the shared crdt_sync transport
(v0.2.0) into the fixed private repo kuhyx/syncs, replacing the bespoke
Gist. AlarmSyncService is the sole writer: it holds the alarm locally in a
crdt_sync LogStore (one HLC-stamped Record) and pushes it to
wake-alarm-sync/devices/phone/alarm.json. gist_service.dart is deleted;
SyncSettings drops the Gist id and keeps only a token; the device-flow scope
moves from `gist` to `repo` (the Contents API needs private-repo access).
shutdown-wrapper.sh: the PC reader now fetches that alarm from the Contents
API (Accept: application/vnd.github.raw, so no base64) and parses the
crdt_sync Record Log with jq (.alarm.fields.alarm_hour[0]). Single-file read
of the sole "phone" device; unchanged fallback to WAKE_AFTER_HOURS on any
miss so a parse failure can never brick the wake. Config moves from
gist_sync.json {token, gist_id} to sync.json {token}.
Verified: flutter analyze clean + tests pass; shellcheck clean and the full
get_synced_alarm_epoch verified against the real Log format; on-device the
app builds, installs with adb install -r (no data loss), launches and
renders. Live round-trip needs a one-time repo-scoped re-auth on the phone
and ~/.config/wake_alarm/sync.json on the PC.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
99 lines
3.5 KiB
Dart
99 lines
3.5 KiB
Dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// Locally-stored GitHub sync configuration for wake_alarm_sync.
|
|
///
|
|
/// Only a GitHub token is stored now: the alarm syncs through the shared
|
|
/// `crdt_sync` transport into the fixed private repo `kuhyx/syncs` (see
|
|
/// `AlarmSyncService`), so there is no per-install Gist id to keep. The token
|
|
/// is kept in the OS keystore (Android Keystore) via [FlutterSecureStorage].
|
|
/// Legacy builds stored the token in plaintext prefs (and a `gist_id`);
|
|
/// [load]/[save] migrate the token transparently and drop the stale keys.
|
|
class SyncSettings {
|
|
/// Creates a [SyncSettings].
|
|
const SyncSettings({required this.token});
|
|
|
|
/// GitHub access token (device-flow token or PAT) with `repo` scope so the
|
|
/// Contents API can read/write the private `kuhyx/syncs` repo.
|
|
final String token;
|
|
|
|
/// Baked-in OAuth App client ID for the device-flow "Connect" button.
|
|
/// Device-flow client IDs are public identifiers, not secrets.
|
|
static const defaultClientId = 'Ov23liGUPrPe1FFRSel6';
|
|
|
|
/// True when a token is present and sync can proceed.
|
|
bool get isConfigured => token.isNotEmpty;
|
|
|
|
// Legacy plaintext key — read-only; removed once migrated to keystore.
|
|
static const _kLegacyToken = 'github_token';
|
|
// Legacy Gist id key — no longer used; cleared on load/save/clear.
|
|
static const _kLegacyGistId = 'gist_id';
|
|
static const _secureTokenKey = 'github_token';
|
|
|
|
static const _secure = FlutterSecureStorage();
|
|
|
|
/// Loads settings from persistent storage.
|
|
static Future<SyncSettings> load() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
return SyncSettings(token: await _loadToken(prefs));
|
|
}
|
|
|
|
/// Reads the token from the keystore, falling back to the legacy plaintext
|
|
/// value. A legacy value is migrated into the keystore on read, but only
|
|
/// deleted from prefs once that write succeeds.
|
|
static Future<String> _loadToken(SharedPreferences prefs) async {
|
|
String? secure;
|
|
try {
|
|
secure = await _secure.read(key: _secureTokenKey);
|
|
} on Exception catch (_) {
|
|
secure = null;
|
|
}
|
|
if (secure != null && secure.isNotEmpty) return secure;
|
|
|
|
final legacy = prefs.getString(_kLegacyToken) ?? '';
|
|
if (legacy.isNotEmpty && await _writeSecureToken(legacy)) {
|
|
await prefs.remove(_kLegacyToken);
|
|
}
|
|
return legacy;
|
|
}
|
|
|
|
/// Persists current settings and drops the stale Gist id.
|
|
Future<void> save() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(_kLegacyGistId);
|
|
if (await _writeSecureToken(token)) {
|
|
await prefs.remove(_kLegacyToken);
|
|
} else {
|
|
await prefs.setString(_kLegacyToken, token);
|
|
}
|
|
}
|
|
|
|
/// Removes all stored credentials.
|
|
static Future<void> clear() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(_kLegacyGistId);
|
|
await prefs.remove(_kLegacyToken);
|
|
await _writeSecureToken('');
|
|
}
|
|
|
|
/// Writes [token] to the keystore. Returns false if the platform secret
|
|
/// service is unavailable.
|
|
static Future<bool> _writeSecureToken(String token) async {
|
|
try {
|
|
if (token.isEmpty) {
|
|
await _secure.delete(key: _secureTokenKey);
|
|
} else {
|
|
await _secure.write(key: _secureTokenKey, value: token);
|
|
}
|
|
return true;
|
|
} on Exception catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Returns a copy with optional field overrides.
|
|
SyncSettings copyWith({String? token}) {
|
|
return SyncSettings(token: token ?? this.token);
|
|
}
|
|
}
|