utils/github_device_auth
Krzysztof kuhy Rudnicki 200432a2b4 dart: split ten oversized test files by group
firebase_auth_rest_test.dart  525 -> 153 + _google 158 + _tokens 206 + _uid 141
  sync_revisions_test.dart      451 -> 223 + _peers 228 + _backends 158
                                       + _order 165 + _persistence 215
  sync_settings_screen_test     457 -> 207 + _backup 143 + _google 145
  mirror_store_test.dart        452 -> 209 + _reads 172 + _fallback 196 + _revisions 244
  firebase_client_test.dart     374 -> 198 + _writes 227
  firebase_config_test.dart     360 -> 138 + _client 148 + _client_google 164
  device_auth_test.dart         307 -> 128 + _poll 201
  github_client_test.dart       291 -> 143 + _writes 201
  sync_test.dart                269 -> 160 + _skips 175

Two traps this hit, both invisible to a line count:

  - _FakePersistence is declared *after* main(), so a naive cut at the last
    brace dropped it; sync_revisions_persistence_test.dart failed to load
    until it was carried across. Audited every other split file for trailing
    top-level declarations — this was the only one.
  - Copying the preamble into each sibling left helpers the sibling never
    uses. `dart analyze` flagged 12; all removed rather than left as dead
    code, and the analyzer is now clean.

crdt_sync_dart 280 passed, sync_settings_ui 29 passed, github_device_auth
analyze clean — all matching a clean HEAD worktree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:54:35 +02:00
..
lib github_device_auth: retry transient network failures (v0.3.0) 2026-08-15 16:05:12 +02:00
scripts github_device_auth: extract the GitHub device flow from four apps 2026-08-15 15:57:19 +02:00
test dart: split ten oversized test files by group 2026-08-22 14:54:35 +02:00
.gitignore github_device_auth: extract the GitHub device flow from four apps 2026-08-15 15:57:19 +02:00
analysis_options.yaml github_device_auth: extract the GitHub device flow from four apps 2026-08-15 15:57:19 +02:00
pubspec.lock github_device_auth: extract the GitHub device flow from four apps 2026-08-15 15:57:19 +02:00
pubspec.yaml github_device_auth: retry transient network failures (v0.3.0) 2026-08-15 16:05:12 +02:00
README.md github_device_auth: retry transient network failures (v0.3.0) 2026-08-15 16:05:12 +02:00

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

dependencies:
  github_device_auth:
    git:
      url: https://github.com/kuhyx/utils
      ref: github_device_auth-v0.3.0
      path: github_device_auth
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

flutter pub get
flutter test              # 24 tests
flutter test --coverage   # 100% of lines
flutter analyze           # very_good_analysis, clean