From 3d23e6b70af9ae44423a81b2c04816ab1f2a9c41 Mon Sep 17 00:00:00 2001 From: Julian Dice <19397727+windoze95@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:26:57 -0500 Subject: [PATCH] feat: remove the ChatGPT sign-in from the AI providers panel Follows the backend removal of the Codex/ChatGPT OAuth rank provider (it couldn't do embeddings, so it was dropped). Removes the client surface: - _ChatGptSignInCard (device-flow widget) and its use in the section - ChatgptLoginStatus / ChatgptPollResult models - getChatgptLogin / startChatgptLogin / pollChatgptLogin / clearChatgptLogin api_service methods + the chatgpt-login endpoint constants - the now-unused dart:async and browser_link imports - test references The AI providers panel is otherwise unchanged: per-provider keys (Anthropic/Gemini/OpenAI) and the embed/rank selection. Co-Authored-By: Claude Fable 5 --- lib/config/constants.dart | 3 - lib/models/ai_providers.dart | 54 +--- lib/services/api_service.dart | 30 -- lib/widgets/ai_providers_section.dart | 298 +------------------- test/widgets/ai_providers_section_test.dart | 7 +- 5 files changed, 7 insertions(+), 385 deletions(-) diff --git a/lib/config/constants.dart b/lib/config/constants.dart index 038f3a3..8c3e406 100644 --- a/lib/config/constants.dart +++ b/lib/config/constants.dart @@ -47,9 +47,6 @@ class AppConstants { '$apiBase/settings/ai-providers/keys/$provider'; static String settingsAiSelection(String role) => '$apiBase/settings/ai-providers/selection/$role'; - static const String settingsChatgptLogin = '$apiBase/settings/chatgpt-login'; - static const String settingsChatgptLoginPoll = - '$apiBase/settings/chatgpt-login/poll'; static const String channels = '$apiBase/channels'; static const String channelSubscribe = '$apiBase/channels/subscribe'; static const String channelSubscribeBulk = '$apiBase/channels/subscribe-bulk'; diff --git a/lib/models/ai_providers.dart b/lib/models/ai_providers.dart index 1170505..3ada339 100644 --- a/lib/models/ai_providers.dart +++ b/lib/models/ai_providers.dart @@ -82,7 +82,7 @@ class AiProvidersStatus { final AiSelection embed; final AiSelection rank; - /// Per-provider "usable right now", including `chatgpt` (sign-in based). + /// Per-provider "usable right now" (anthropic / gemini / openai). final Map availability; factory AiProvidersStatus.fromJson(Map json) { @@ -104,55 +104,3 @@ class AiProvidersStatus { ); } } - -/// Status of the ChatGPT (Codex OAuth) sign-in. -class ChatgptLoginStatus { - const ChatgptLoginStatus({ - required this.connected, - required this.needsReauth, - required this.pending, - required this.userCode, - required this.verificationUrl, - }); - - final bool connected; - final bool needsReauth; - final bool pending; - - /// Device code to enter (only while a sign-in is pending). - final String? userCode; - final String? verificationUrl; - - factory ChatgptLoginStatus.fromJson(Map json) => - ChatgptLoginStatus( - connected: json['connected'] == true, - needsReauth: json['needs_reauth'] == true, - pending: json['pending'] == true, - userCode: json['user_code'] as String?, - verificationUrl: json['verification_url'] as String?, - ); -} - -/// One poll of the device sign-in. -class ChatgptPollResult { - const ChatgptPollResult({ - required this.status, - required this.detail, - required this.userCode, - required this.verificationUrl, - }); - - /// idle | pending | connected | expired | error - final String status; - final String? detail; - final String? userCode; - final String? verificationUrl; - - factory ChatgptPollResult.fromJson(Map json) => - ChatgptPollResult( - status: (json['status'] as String?) ?? 'idle', - detail: json['detail'] as String?, - userCode: json['user_code'] as String?, - verificationUrl: json['verification_url'] as String?, - ); -} diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index d705baf..c7158b7 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -531,36 +531,6 @@ class ApiService { return AiProvidersStatus.fromJson(r.data as Map); }); - // --- ChatGPT (Codex OAuth) sign-in (admin) -------------------------------- - - Future getChatgptLogin() => _guard(() async { - final r = await _dio.get('$_baseUrl${AppConstants.settingsChatgptLogin}'); - return ChatgptLoginStatus.fromJson(r.data as Map); - }); - - Future startChatgptLogin() => _guard(() async { - final r = await _dio.post('$_baseUrl${AppConstants.settingsChatgptLogin}'); - // The start response has the same shape as a pending poll. - final data = r.data as Map; - return ChatgptPollResult( - status: 'pending', - detail: null, - userCode: data['user_code'] as String?, - verificationUrl: data['verification_url'] as String?, - ); - }); - - Future pollChatgptLogin() => _guard(() async { - final r = await _dio.post( - '$_baseUrl${AppConstants.settingsChatgptLoginPoll}', - ); - return ChatgptPollResult.fromJson(r.data as Map); - }); - - Future clearChatgptLogin() => _guard(() async { - await _dio.delete('$_baseUrl${AppConstants.settingsChatgptLogin}'); - }); - /// Records an evictable cache claim on [videoId] and (server-side) kicks off /// its HQ download, without it showing in the Downloads tab. Called when the /// user starts instant playback of a not-yet-downloaded video so the player diff --git a/lib/widgets/ai_providers_section.dart b/lib/widgets/ai_providers_section.dart index 2fb0773..398f4da 100644 --- a/lib/widgets/ai_providers_section.dart +++ b/lib/widgets/ai_providers_section.dart @@ -1,17 +1,13 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../config/theme.dart'; import '../models/ai_providers.dart'; import '../services/api_service.dart'; -import '../utils/browser_link.dart'; /// Admin panel for the Discover AI providers: API keys (Anthropic / Gemini / -/// OpenAI), the embed + rank provider selection, and the ChatGPT sign-in that -/// backs the subscription-based rank provider. Values are stored server-side; -/// keys are write-only (never read back). +/// OpenAI) and the embed + rank provider selection. Values are stored +/// server-side; keys are write-only (never read back). class AiProvidersSection extends ConsumerStatefulWidget { const AiProvidersSection({super.key}); @@ -25,7 +21,6 @@ class _AiProvidersSectionState extends ConsumerState { 'anthropic': 'Anthropic', 'gemini': 'Google Gemini', 'openai': 'OpenAI', - 'chatgpt': 'ChatGPT sign-in', }; AiProvidersStatus? _status; @@ -93,11 +88,7 @@ class _AiProvidersSectionState extends ConsumerState { ), const SizedBox(height: 12), ], - _ChatGptSignInCard( - available: status.availability['chatgpt'] ?? false, - onChanged: _load, - ), - const SizedBox(height: 16), + const SizedBox(height: 4), _SelectionCard( role: 'embed', title: 'Embedding provider', @@ -112,8 +103,8 @@ class _AiProvidersSectionState extends ConsumerState { role: 'rank', title: 'Ranking provider', description: - 'Picks and explains the final recommendations. Can use any key ' - 'above, or the ChatGPT sign-in.', + 'Picks and explains the final recommendations. Uses any of ' + 'the keys above.', selection: status.rank, onSaved: _apply, ), @@ -408,282 +399,3 @@ class _SelectionCardState extends ConsumerState<_SelectionCard> { ); } } - -class _ChatGptSignInCard extends ConsumerStatefulWidget { - const _ChatGptSignInCard({required this.available, required this.onChanged}); - - final bool available; - - /// Called after a state change (connected / signed out) so the parent can - /// refresh availability + effective-provider labels. - final Future Function() onChanged; - - @override - ConsumerState<_ChatGptSignInCard> createState() => _ChatGptSignInCardState(); -} - -class _ChatGptSignInCardState extends ConsumerState<_ChatGptSignInCard> { - ChatgptLoginStatus? _status; - Timer? _poller; - String? _userCode; - String? _verificationUrl; - bool _busy = false; - String? _error; - - ApiService get _api => ref.read(apiServiceProvider); - - @override - void initState() { - super.initState(); - _load(); - } - - @override - void dispose() { - _poller?.cancel(); - super.dispose(); - } - - Future _load() async { - try { - final s = await _api.getChatgptLogin(); - if (!mounted) return; - setState(() { - _status = s; - if (s.pending) { - _userCode = s.userCode; - _verificationUrl = s.verificationUrl; - _startPolling(); - } - }); - } catch (_) { - /* leave as unknown */ - } - } - - Future _connect() async { - setState(() { - _busy = true; - _error = null; - }); - try { - final r = await _api.startChatgptLogin(); - if (!mounted) return; - setState(() { - _busy = false; - _userCode = r.userCode; - _verificationUrl = r.verificationUrl; - }); - if (r.verificationUrl != null) openInNewTab(r.verificationUrl!); - _startPolling(); - } on ApiException catch (e) { - if (mounted) { - setState(() { - _busy = false; - _error = e.message; - }); - } - } - } - - void _startPolling() { - _poller?.cancel(); - _poller = Timer.periodic(const Duration(seconds: 3), (_) => _poll()); - } - - Future _poll() async { - ChatgptPollResult r; - try { - r = await _api.pollChatgptLogin(); - } catch (_) { - return; // transient; keep polling - } - if (!mounted) return; - switch (r.status) { - case 'connected': - _poller?.cancel(); - setState(() { - _userCode = null; - _verificationUrl = null; - }); - await _load(); - await widget.onChanged(); - break; - case 'pending': - setState(() { - _userCode = r.userCode ?? _userCode; - _verificationUrl = r.verificationUrl ?? _verificationUrl; - }); - break; - case 'expired': - case 'error': - case 'idle': - _poller?.cancel(); - setState(() { - _userCode = null; - _verificationUrl = null; - _error = r.detail; - }); - break; - } - } - - Future _disconnect() async { - setState(() => _busy = true); - try { - await _api.clearChatgptLogin(); - _poller?.cancel(); - if (!mounted) return; - setState(() { - _busy = false; - _userCode = null; - _verificationUrl = null; - }); - await _load(); - await widget.onChanged(); - } catch (_) { - if (mounted) setState(() => _busy = false); - } - } - - @override - Widget build(BuildContext context) { - final status = _status; - final connected = status?.connected == true; - final signingIn = _userCode != null; - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - connected - ? Icons.check_circle_outline_rounded - : Icons.circle_outlined, - size: 18, - color: connected - ? NullFeedTheme.accentColor - : NullFeedTheme.textMuted, - ), - const SizedBox(width: 8), - Text( - 'ChatGPT subscription', - style: Theme.of(context).textTheme.titleSmall, - ), - const Spacer(), - if (status?.needsReauth == true) - const Text( - 'reconnect', - style: TextStyle( - color: NullFeedTheme.errorColor, - fontSize: 12, - ), - ), - ], - ), - const SizedBox(height: 6), - Text( - 'Rank Discover on a ChatGPT Plus/Pro plan instead of an OpenAI ' - 'key. Enable device authorization in ChatGPT → Settings → ' - 'Security first. Shares your plan\'s Codex usage limits.', - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: NullFeedTheme.textMuted), - ), - if (signingIn) ...[const SizedBox(height: 12), _codePanel()], - if (_error != null) ...[ - const SizedBox(height: 8), - Text( - _error!, - style: const TextStyle( - color: NullFeedTheme.errorColor, - fontSize: 12, - ), - ), - ], - const SizedBox(height: 12), - Row( - children: [ - if (!connected && !signingIn) - FilledButton( - onPressed: _busy ? null : _connect, - child: _busy - ? const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: NullFeedTheme.textSecondary, - ), - ) - : const Text('Connect'), - ), - if (signingIn) - const Text( - 'Waiting for approval…', - style: TextStyle( - color: NullFeedTheme.textMuted, - fontSize: 13, - ), - ), - if (connected) - TextButton( - onPressed: _busy ? null : _disconnect, - child: const Text( - 'Sign out', - style: TextStyle(color: NullFeedTheme.errorColor), - ), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _codePanel() { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: NullFeedTheme.elevatedSurfaceColor, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Enter this code at the ChatGPT page:'), - const SizedBox(height: 6), - SelectableText( - _userCode ?? '', - style: const TextStyle( - fontFamily: 'monospace', - fontSize: 22, - letterSpacing: 2, - color: NullFeedTheme.textPrimary, - ), - ), - if (_verificationUrl != null) ...[ - const SizedBox(height: 6), - Align( - alignment: Alignment.centerLeft, - child: TextButton.icon( - onPressed: () => openInNewTab(_verificationUrl!), - icon: const Icon(Icons.open_in_new_rounded, size: 16), - label: const Text('Open the sign-in page'), - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ), - ), - ], - ], - ), - ); - } -} diff --git a/test/widgets/ai_providers_section_test.dart b/test/widgets/ai_providers_section_test.dart index 20669c2..564215c 100644 --- a/test/widgets/ai_providers_section_test.dart +++ b/test/widgets/ai_providers_section_test.dart @@ -13,7 +13,6 @@ AiProvidersStatus _status({ Map? geminiKey, String rankProvider = '', Map? rankEffective, - bool chatgpt = false, }) => AiProvidersStatus.fromJson({ 'keys': { 'anthropic': {'configured': false, 'source': null, 'last4': null}, @@ -32,13 +31,12 @@ AiProvidersStatus _status({ 'model': '', 'source': rankProvider.isEmpty ? 'env' : 'runtime', 'effective': rankEffective, - 'options': ['anthropic', 'gemini', 'openai', 'chatgpt'], + 'options': ['anthropic', 'gemini', 'openai'], }, 'availability': { 'anthropic': false, 'gemini': geminiKey?['configured'] == true, 'openai': false, - 'chatgpt': chatgpt, }, }); @@ -47,9 +45,6 @@ void main() { setUp(() { api = MockApiService(); - when(() => api.getChatgptLogin()).thenAnswer( - (_) async => ChatgptLoginStatus.fromJson({'connected': false}), - ); }); Future pump(WidgetTester tester) async {