import 'dart:convert'; import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; import 'package:real_estate_mobile/config/app_config.dart'; /// Twitter OAuth 2.0 PKCE flow for mobile. class TwitterAuthService { static const _clientId = 'eWEzaDJ0al9lX1diOGY0ejRYeXM6MTpjaQ'; static const _scope = 'tweet.read users.read offline.access'; /// Derive redirect URI from API base URL (matches next-auth callback) static String get _redirectUri { final apiUrl = AppConfig.apiBaseUrl; // e.g. https://dev.re-quest.com/api/v1 final base = apiUrl.replaceAll(RegExp(r'/api/v1/?$'), ''); return '$base/api/auth/callback/twitter'; } late final String _codeVerifier; late final String _codeChallenge; late final String _stateParam; TwitterAuthService() { _codeVerifier = _generateCodeVerifier(); _codeChallenge = _generateCodeChallenge(_codeVerifier); _stateParam = _generateRandomString(32); } /// Build the Twitter authorization URL to open in a webview. String get authorizationUrl { final params = { 'response_type': 'code', 'client_id': _clientId, 'redirect_uri': _redirectUri, 'scope': _scope, 'state': _stateParam, 'code_challenge': _codeChallenge, 'code_challenge_method': 'S256', }; final query = params.entries .map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') .join('&'); return 'https://twitter.com/i/oauth2/authorize?$query'; } /// Check if a URL is the redirect callback and extract the auth code. String? extractCodeFromUrl(String url) { if (!url.startsWith(_redirectUri)) return null; final uri = Uri.parse(url); final state = uri.queryParameters['state']; if (state != _stateParam) return null; return uri.queryParameters['code']; } /// Exchange the auth code for tokens and fetch user info. Future?> exchangeCodeForUser(String code) async { final dio = Dio(); // Exchange code for access token (PKCE public client — client_id in body) final tokenBody = 'code=${Uri.encodeComponent(code)}' '&grant_type=authorization_code' '&client_id=${Uri.encodeComponent(_clientId)}' '&redirect_uri=${Uri.encodeComponent(_redirectUri)}' '&code_verifier=${Uri.encodeComponent(_codeVerifier)}'; final tokenResponse = await dio.post( 'https://api.twitter.com/2/oauth2/token', options: Options( contentType: 'application/x-www-form-urlencoded', ), data: tokenBody, ); final accessToken = tokenResponse.data['access_token'] as String?; if (accessToken == null) return null; // Fetch user info final userResponse = await dio.get( 'https://api.twitter.com/2/users/me', queryParameters: {'user.fields': 'id,name,username,profile_image_url'}, options: Options( headers: {'Authorization': 'Bearer $accessToken'}, ), ); final userData = userResponse.data['data'] as Map?; if (userData == null) return null; return { 'id': userData['id'], 'name': userData['name'], 'username': userData['username'], 'avatar': userData['profile_image_url'], }; } String _generateRandomString(int length) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; final random = Random.secure(); return List.generate(length, (_) => chars[random.nextInt(chars.length)]).join(); } String _generateCodeVerifier() => _generateRandomString(64); String _generateCodeChallenge(String verifier) { final bytes = utf8.encode(verifier); final digest = sha256.convert(bytes); return base64Url.encode(digest.bytes).replaceAll('=', ''); } }