utils/github_device_auth/README.md
Krzysztof kuhy Rudnicki 795a8eae62 github_device_auth: extract the GitHub device flow from four apps
todo, home_inventory, diet-guard and wake-alarm each carried the OAuth
device-flow client and the modal that drives it -- ~650 lines, with two
pairs byte-for-byte identical (todo/home_inventory's clients, and
diet-guard/wake-alarm's dialogs).

Deliberately its own package rather than part of `sync_settings_ui`. That
package is Firebase-only by an explicit 2026-08-12 decision recorded as
"do not re-add a GitHub section without kuhy asking again", so folding a
GitHub flow into it would reverse a standing decision. The GitHub mirror
in these apps is legacy-but-load-bearing code that was explicitly not
authorised for deletion, so it gets its own home instead of being merged
into the Firebase path or dropped.

One behaviour change, made once here instead of four times: the dialog
catches `on Object`, not `on Exception`. An `Error` is not an `Exception`
in Dart, so `ArgumentError` -- notably from URI parsing -- escaped
`_poll()` and left the dialog spinning on "Waiting for authorization…"
forever with nothing shown. That is the same trap that silently killed the
Firebase sync tick across these apps in 2026-08. A test pins it with a
mock that throws an Error rather than an Exception, so it cannot regress
back to `on Exception` unnoticed.

20 tests, 100% line coverage, clean under very_good_analysis, with the
same CI gate the other Dart package holds. Consumers are migrated
separately so each app's own suite gates its own swap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 15:57:19 +02:00

70 lines
2.5 KiB
Markdown

# `github_device_auth`
GitHub OAuth **Device Flow** for kuhy's Flutter apps: the auth client and the
modal that drives it.
Device flow needs only a public `client_id` (no client secret), which is what
makes it safe for a distributed app. The resulting access token is then used
exactly like a PAT.
## Why this is its own package
Four apps each carried a copy of both halves:
| App | Client | Dialog |
| --- | --- | --- |
| todo | `lib/sync/github_device_auth.dart` (154) | `lib/ui/device_code_dialog.dart` (110) |
| home_inventory | `lib/sync/github_device_auth.dart` (154, **byte-identical** to todo's) | inline |
| diet-guard | `app/lib/services/github_device_auth.dart` (175) | inline (~84, **byte-identical** to wake-alarm's) |
| wake-alarm | `phone_app/lib/services/github_device_auth.dart` (166) | inline (~84) |
~650 lines, two pairs of them byte-for-byte identical.
**It is deliberately not part of `sync_settings_ui`.** That package is
Firebase-only by an explicit decision (2026-08-12), recorded with "do not
re-add a GitHub section without kuhy asking again". Folding a GitHub flow into
it would reverse that. The GitHub mirror in these apps is legacy-but-
load-bearing code, so it gets its own home rather than being merged into the
Firebase path — or deleted, which was never authorised.
## Use
```yaml
dependencies:
github_device_auth:
git:
url: https://github.com/kuhyx/utils
ref: github_device_auth-v0.1.0
path: github_device_auth
```
```dart
final auth = GitHubDeviceAuth(clientId: '<oauth app client id>');
final device = await auth.requestDeviceCode();
final token = await showDialog<String>(
context: context,
builder: (_) => DeviceCodeDialog(device: device, auth: auth),
);
auth.close();
```
## One behaviour change from the extracted copies
The dialog now catches `on Object`, not `on Exception`.
An `Error` is not an `Exception` in Dart, so `on Exception` let `ArgumentError`
(notably from URI parsing) escape `_poll()` — leaving the dialog spinning on
"Waiting for authorization…" forever with nothing shown to the user. This is
the same trap that silently killed the Firebase sync tick across these apps in
2026-08. Fixed once here rather than four times. `test/device_code_dialog_test.dart`
pins it with a mock that throws an `Error` rather than an `Exception`.
## Commands
```bash
flutter pub get
flutter test # 20 tests
flutter test --coverage # 100% of lines
flutter analyze # very_good_analysis, clean
```