@@ -3,11 +3,12 @@ import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../models/trakt/trakt_device_code.dart';
|
||||
import '../../models/trackers/device_code.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/platform_http_client_stub.dart'
|
||||
if (dart.library.io) '../../utils/platform_http_client_io.dart'
|
||||
as platform;
|
||||
import '../trackers/device_code_poller.dart' as poller;
|
||||
import 'trakt_constants.dart';
|
||||
|
||||
/// Trakt OAuth Device Authorization Grant flow (RFC 8628).
|
||||
@@ -22,9 +23,9 @@ class TraktAuthService {
|
||||
|
||||
void dispose() => _http.close();
|
||||
|
||||
/// Request a device code. The returned [TraktDeviceCode.userCode] is what
|
||||
/// the user must enter at [TraktDeviceCode.verificationUrl].
|
||||
Future<TraktDeviceCode> createDeviceCode() async {
|
||||
/// Request a device code. The returned [DeviceCode.userCode] is what the
|
||||
/// user must enter at [DeviceCode.verificationUrl].
|
||||
Future<DeviceCode> createDeviceCode() async {
|
||||
final uri = Uri.parse(TraktConstants.deviceCodeUrl);
|
||||
final sw = Stopwatch()..start();
|
||||
final res = await _http
|
||||
@@ -37,86 +38,61 @@ class TraktAuthService {
|
||||
throw TraktAuthFlowException('Device code request failed: HTTP ${res.statusCode}: ${res.body}');
|
||||
}
|
||||
|
||||
return TraktDeviceCode.fromJson(json.decode(res.body) as Map<String, dynamic>);
|
||||
final body = json.decode(res.body) as Map<String, dynamic>;
|
||||
final verificationUrl = body['verification_url'] as String;
|
||||
final userCode = body['user_code'] as String;
|
||||
return DeviceCode(
|
||||
deviceCode: body['device_code'] as String,
|
||||
userCode: userCode,
|
||||
verificationUrl: verificationUrl,
|
||||
verificationUrlComplete: '$verificationUrl/$userCode',
|
||||
expiresIn: (body['expires_in'] as num).toInt(),
|
||||
interval: (body['interval'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Poll the device-token endpoint until the user authorizes, denies, or the
|
||||
/// code expires. Yields one event per poll attempt; the stream completes
|
||||
/// after the first terminal event ([TraktDevicePollSuccess],
|
||||
/// [TraktDevicePollDenied], [TraktDevicePollExpired]).
|
||||
///
|
||||
/// Pass [shouldCancel] to abort polling early (e.g. user dismissed the
|
||||
/// dialog).
|
||||
Stream<TraktDevicePollEvent> pollDeviceCode(TraktDeviceCode code, {bool Function()? shouldCancel}) async* {
|
||||
var interval = Duration(seconds: code.interval);
|
||||
final deadline = DateTime.now().add(Duration(seconds: code.expiresIn));
|
||||
/// code expires. Pass [shouldCancel] to abort early.
|
||||
Stream<DevicePollEvent> pollDeviceCode(DeviceCode code, {bool Function()? shouldCancel}) {
|
||||
return poller.pollDeviceCode(code, shouldCancel: shouldCancel, probe: () => _probe(code));
|
||||
}
|
||||
|
||||
while (DateTime.now().isBefore(deadline)) {
|
||||
if (shouldCancel != null && shouldCancel()) return;
|
||||
|
||||
await Future<void>.delayed(interval);
|
||||
|
||||
if (shouldCancel != null && shouldCancel()) return;
|
||||
|
||||
http.Response res;
|
||||
final tokenUri = Uri.parse(TraktConstants.deviceTokenUrl);
|
||||
final sw = Stopwatch()..start();
|
||||
try {
|
||||
res = await _http
|
||||
.post(
|
||||
tokenUri,
|
||||
headers: TraktConstants.headers(),
|
||||
body: json.encode({
|
||||
'code': code.deviceCode,
|
||||
'client_id': TraktConstants.clientId,
|
||||
'client_secret': TraktConstants.clientSecret,
|
||||
}),
|
||||
)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
sw.stop();
|
||||
appLogger.d('Trakt POST ${tokenUri.path} → ${res.statusCode} (${sw.elapsedMilliseconds}ms)');
|
||||
} catch (e) {
|
||||
appLogger.d('Trakt device-code poll error (transient)', error: e);
|
||||
yield const TraktDevicePollPending();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (res.statusCode) {
|
||||
case 200:
|
||||
final body = json.decode(res.body) as Map<String, dynamic>;
|
||||
yield TraktDevicePollSuccess(body);
|
||||
return;
|
||||
case 400:
|
||||
// Pending — keep polling
|
||||
yield const TraktDevicePollPending();
|
||||
break;
|
||||
case 404:
|
||||
// Code not found / expired
|
||||
yield const TraktDevicePollExpired();
|
||||
return;
|
||||
case 409:
|
||||
// Already used
|
||||
yield const TraktDevicePollDenied();
|
||||
return;
|
||||
case 410:
|
||||
yield const TraktDevicePollExpired();
|
||||
return;
|
||||
case 418:
|
||||
// Denied
|
||||
yield const TraktDevicePollDenied();
|
||||
return;
|
||||
case 429:
|
||||
// Slow down — increase interval by 5s
|
||||
interval += const Duration(seconds: 5);
|
||||
yield const TraktDevicePollSlowDown();
|
||||
break;
|
||||
default:
|
||||
appLogger.w('Trakt device-code unexpected HTTP ${res.statusCode}: ${res.body}');
|
||||
yield const TraktDevicePollPending();
|
||||
}
|
||||
Future<DevicePollEvent> _probe(DeviceCode code) async {
|
||||
final tokenUri = Uri.parse(TraktConstants.deviceTokenUrl);
|
||||
final http.Response res;
|
||||
try {
|
||||
res = await _http
|
||||
.post(
|
||||
tokenUri,
|
||||
headers: TraktConstants.headers(),
|
||||
body: json.encode({
|
||||
'code': code.deviceCode,
|
||||
'client_id': TraktConstants.clientId,
|
||||
'client_secret': TraktConstants.clientSecret,
|
||||
}),
|
||||
)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
appLogger.d('Trakt POST ${tokenUri.path} → ${res.statusCode}');
|
||||
} catch (e) {
|
||||
appLogger.d('Trakt device-code poll error (transient)', error: e);
|
||||
return const DevicePollPending();
|
||||
}
|
||||
|
||||
yield const TraktDevicePollExpired();
|
||||
switch (res.statusCode) {
|
||||
case 200:
|
||||
return DevicePollSuccess(json.decode(res.body) as Map<String, dynamic>);
|
||||
case 400:
|
||||
return const DevicePollPending();
|
||||
case 404 || 410:
|
||||
return const DevicePollExpired();
|
||||
case 409 || 418:
|
||||
return const DevicePollDenied();
|
||||
case 429:
|
||||
return const DevicePollSlowDown();
|
||||
default:
|
||||
appLogger.w('Trakt device-code unexpected HTTP ${res.statusCode}: ${res.body}');
|
||||
return const DevicePollPending();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user