mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 22:07:15 +01:00
wake-alarm's copy carried a fix the other three lacked: `pollForToken` swallows a transient socket failure and keeps polling, because GitHub can close the connection at the exact moment the user authorizes and a phone can drop its network mid-poll. Without it, an approved grant is lost to a blip and the user has to start the whole flow again. Extracting only todo's version would have silently deleted that fix from wake-alarm. Taking it into the package instead gives it to all four apps. Catching `http.ClientException` is sufficient, and avoiding `dart:io` here is deliberate: diet-guard's desktop **web** build imports this package, and naming `SocketException` would break that compile. Checked rather than assumed -- a probe against a dead port shows package:http raises `_ClientSocketException`, which implements *both* `ClientException` and `SocketException`, so the single catch covers IO and web alike. 24 tests, 100% line coverage, clean analyze. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
92 lines
3.5 KiB
Markdown
92 lines
3.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.3.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();
|
|
```
|
|
|
|
## Configurable endpoints (web builds)
|
|
|
|
`deviceCodeUrl` / `tokenUrl` default to GitHub's own URLs, which is right for
|
|
mobile and desktop. A **web** build must point both at a local proxy instead:
|
|
GitHub's device-flow endpoints send no CORS headers, so a page cannot call
|
|
them at all. diet-guard's desktop web build does exactly that, which is why
|
|
this knob exists — it was in diet-guard's local copy and would have been lost
|
|
had the package shipped only todo's simpler version.
|
|
|
|
## Transient-network retry
|
|
|
|
`pollForToken` retries on `ClientException` rather than failing. GitHub can
|
|
close the connection at the exact moment the user authorizes, and a phone can
|
|
drop its network mid-poll — losing an approved grant to a socket blip is a real
|
|
bug wake-alarm had already hit and fixed locally. The other three copies had
|
|
not, so they inherit the fix here.
|
|
|
|
`ClientException` alone is enough and `dart:io` is deliberately *not* imported:
|
|
this package is used by a web build, and `package:http` wraps a dead socket as
|
|
`_ClientSocketException`, which implements `ClientException` — verified, not
|
|
assumed.
|
|
|
|
## 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 # 24 tests
|
|
flutter test --coverage # 100% of lines
|
|
flutter analyze # very_good_analysis, clean
|
|
```
|