todo-app/lib/sync/sync_settings.dart
Krzysztof kuhy Rudnicki f5d79a6a57 One-tap GitHub connect via a baked-in OAuth App client id
Previously "Connect GitHub" (OAuth device flow) still required entering an
OAuth App client id and owner/repo — friction that returned on every
reinstall once shared_prefs were wiped.

- Bake the app's own device-flow OAuth App client id in as
  SyncSettings.defaultClientId and default to it in load() (alongside the
  existing kuhyx/todo-sync repo default). A device-flow client id is a
  public identifier, not a secret, so it is safe to commit.
- Settings now leads with a single "Connect GitHub" button; the manual
  client-id / token fields and Test connection move under an "Advanced"
  expander. Result: fresh install (or post-reinstall) is one tap →
  authorize the code in the browser → synced. No tokens, no setup.

Note: an OAuth App authorizes with the classic `repo` scope (all repos),
broader than the prior fine-grained PAT — the trade-off for one-tap
device-flow convenience. 151 tests, 100% line coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:21:34 +02:00

78 lines
2.6 KiB
Dart

import 'package:shared_preferences/shared_preferences.dart';
/// Locally-stored GitHub sync configuration.
///
/// NOTE: the token is currently stored in plain `SharedPreferences`. That is
/// acceptable for a personal dogfood build, but should move to
/// `flutter_secure_storage` (Android Keystore / libsecret) before this is
/// considered done. Tracked as a follow-up.
class SyncSettings {
const SyncSettings({
required this.owner,
required this.repo,
required this.token,
this.clientId = '',
});
final String owner;
final String repo;
final String token;
/// GitHub OAuth App client id used by the device-flow "Connect" button.
/// Not a secret (device flow needs no client secret), so it is safe to ship
/// as a compile-time default and commit to source — see [defaultClientId].
final String clientId;
/// The app's own GitHub OAuth App (device-flow enabled) client id, baked in
/// so "Connect GitHub" works with zero setup — even after a reinstall. A
/// device-flow client id is a public identifier, not a secret.
static const defaultClientId = 'Ov23li9tF2R46PqzJgch';
/// True when enough is set to attempt a sync.
bool get isConfigured =>
owner.isNotEmpty && repo.isNotEmpty && token.isNotEmpty;
/// True when device-flow "Connect GitHub" can be offered.
bool get canUseDeviceFlow => clientId.isNotEmpty;
static const _kOwner = 'sync.owner';
static const _kRepo = 'sync.repo';
static const _kToken = 'sync.token';
static const _kClientId = 'sync.clientId';
/// Loads settings, defaulting the repo to `kuhyx/todo-sync` and the client id
/// to the baked-in [defaultClientId] so first run (and any reinstall) needs
/// nothing but a single "Connect GitHub" tap.
static Future<SyncSettings> load() async {
final prefs = await SharedPreferences.getInstance();
return SyncSettings(
owner: prefs.getString(_kOwner) ?? 'kuhyx',
repo: prefs.getString(_kRepo) ?? 'todo-sync',
token: prefs.getString(_kToken) ?? '',
clientId: prefs.getString(_kClientId) ?? defaultClientId,
);
}
Future<void> save() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kOwner, owner);
await prefs.setString(_kRepo, repo);
await prefs.setString(_kToken, token);
await prefs.setString(_kClientId, clientId);
}
SyncSettings copyWith({
String? owner,
String? repo,
String? token,
String? clientId,
}) {
return SyncSettings(
owner: owner ?? this.owner,
repo: repo ?? this.repo,
token: token ?? this.token,
clientId: clientId ?? this.clientId,
);
}
}