From dfe708a0eb12ebb4a2b090d83c777620620707d5 Mon Sep 17 00:00:00 2001 From: 0chencc <19362246+0Chencc@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:53:59 +0800 Subject: [PATCH] feat: add SharedPreferences storage for macOS/iOS platforms Add alternative data storage implementation using SharedPreferences for platforms where Keychain/SecureStorage is problematic during development. This includes: - Add AuthSharedPrefsDataSource for token storage - Add CredentialsSharedPrefsDataSource for credentials storage - Update DI providers to support alternative storage - Update iOS/macOS platform configurations --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 43 ++++ ios/Runner/Info.plist | 13 ++ lib/application/di/auth_providers.dart | 69 +++++++ lib/application/di/core_providers.dart | 68 ++++++- .../auth_shared_prefs_datasource.dart | 82 ++++++++ .../credentials_shared_prefs_datasource.dart | 191 ++++++++++++++++++ .../repositories/auth_repository_impl.dart | 63 +++--- .../screens/setup_guide_screen.dart | 2 + lib/presentation/themes/app_theme.dart | 6 + macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Podfile | 42 ++++ macos/Podfile.lock | 67 ++++++ macos/Runner.xcodeproj/project.pbxproj | 98 ++++++++- .../contents.xcworkspacedata | 3 + macos/Runner/AppDelegate.swift | 23 +++ macos/Runner/DebugProfile.entitlements | 4 +- macos/Runner/Info.plist | 13 ++ macos/Runner/Release.entitlements | 6 + 21 files changed, 758 insertions(+), 39 deletions(-) create mode 100644 ios/Podfile create mode 100644 lib/data/datasources/auth_shared_prefs_datasource.dart create mode 100644 lib/data/datasources/credentials_shared_prefs_datasource.dart create mode 100644 macos/Podfile create mode 100644 macos/Podfile.lock diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index ec234f0..3dce246 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -45,5 +45,18 @@ UIApplicationSupportsIndirectInputEvents + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.sfo.fullstop + CFBundleURLSchemes + + fullstop + + + diff --git a/lib/application/di/auth_providers.dart b/lib/application/di/auth_providers.dart index 84de99f..d7e3c01 100644 --- a/lib/application/di/auth_providers.dart +++ b/lib/application/di/auth_providers.dart @@ -1,16 +1,37 @@ +import 'dart:io'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../../core/services/oauth_service.dart'; import '../../data/datasources/credentials_local_datasource.dart'; +import '../../data/datasources/credentials_shared_prefs_datasource.dart'; import '../../data/repositories/auth_repository_impl.dart'; +import '../../domain/entities/proxy_settings.dart'; import '../../domain/repositories/auth_repository.dart'; import 'core_providers.dart'; import 'spotify_providers.dart'; /// Authentication-related providers +// SharedPreferences provider (async initialization required) +final sharedPreferencesProvider = FutureProvider((ref) async { + return await SharedPreferences.getInstance(); +}); + // Credentials Local Data Source +// Uses SharedPreferences on macOS/iOS to avoid Keychain issues during development final credentialsLocalDataSourceProvider = Provider( (ref) { + // On macOS and iOS, use SharedPreferences to avoid Keychain/signing issues + if (Platform.isMacOS || Platform.isIOS) { + final prefsAsync = ref.watch(sharedPreferencesProvider); + return prefsAsync.when( + data: (prefs) => CredentialsSharedPrefsDataSource(prefs), + loading: () => _PlaceholderCredentialsDataSource(), + error: (_, __) => _PlaceholderCredentialsDataSource(), + ); + } + // On other platforms, use secure storage final secureStorage = ref.watch(secureStorageProvider); return CredentialsLocalDataSourceImpl(secureStorage); }, @@ -42,3 +63,51 @@ final authRepositoryProvider = Provider((ref) { oauthService: oauthService, ); }); + +/// Placeholder implementation while SharedPreferences is loading +class _PlaceholderCredentialsDataSource implements CredentialsLocalDataSource { + @override + Future clearGetSongBpmApiKey() async {} + @override + Future clearLlmCredentials() async {} + @override + Future clearAppProxySettings() async {} + @override + Future clearSpotifyCredentials() async {} + @override + Future getAudioFeaturesEnabled() async => false; + @override + Future getGetSongBpmApiKey() async => null; + @override + Future getGpuAccelerationEnabled() async => false; + @override + Future getLlmApiKey() async => null; + @override + Future getLlmBaseUrl() async => null; + @override + Future getLlmModel() async => null; + @override + Future getAppProxySettings() async => const AppProxySettings(); + @override + Future getSpotifyClientId() async => null; + @override + Future getSpotifyClientSecret() async => null; + @override + Future hasGetSongBpmApiKey() async => false; + @override + Future hasLlmConfig() async => false; + @override + Future hasSpotifyCredentials() async => false; + @override + Future saveAppProxySettings(AppProxySettings config) async {} + @override + Future saveLlmCredentials({String apiKey = '', required String model, required String baseUrl}) async {} + @override + Future saveSpotifyCredentials({required String clientId, required String clientSecret}) async {} + @override + Future setAudioFeaturesEnabled(bool enabled) async {} + @override + Future setGetSongBpmApiKey(String apiKey) async {} + @override + Future setGpuAccelerationEnabled(bool enabled) async {} +} diff --git a/lib/application/di/core_providers.dart b/lib/application/di/core_providers.dart index e1457a6..f69c39f 100644 --- a/lib/application/di/core_providers.dart +++ b/lib/application/di/core_providers.dart @@ -4,6 +4,7 @@ import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:socks5_proxy/socks_client.dart' as socks5; import '../../core/config/app_config.dart'; import '../../core/services/deep_link_service.dart'; @@ -13,11 +14,18 @@ import '../../core/services/url_launcher_service.dart'; import '../../core/services/window_service.dart'; import '../../core/utils/logger.dart'; import '../../data/datasources/auth_local_datasource.dart'; +import '../../data/datasources/auth_shared_prefs_datasource.dart'; import '../../data/datasources/credentials_local_datasource.dart'; import '../../domain/entities/proxy_settings.dart'; import '../../data/services/app_links_deep_link_service.dart'; import '../../data/services/default_url_launcher_service.dart'; import '../../data/services/window_manager_service.dart'; +import 'auth_providers.dart' show credentialsLocalDataSourceProvider; + +// SharedPreferences provider for macOS/iOS +final sharedPrefsProvider = FutureProvider((ref) async { + return await SharedPreferences.getInstance(); +}); /// Core infrastructure providers /// These are low-level services that other modules depend on @@ -26,6 +34,10 @@ import '../../data/services/window_manager_service.dart'; final secureStorageProvider = Provider((ref) { return const FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), + mOptions: MacOsOptions(), + iOptions: IOSOptions( + accountName: 'com.sfo.fullstop', + ), ); }); @@ -114,23 +126,17 @@ final apiDioProvider = Provider((ref) { // Configure proxy if available _configureDioProxy(dio, _currentProxyConfig); - final authLocalDataSource = ref.read(authLocalDataSourceProvider); - final secureStorage = ref.read(secureStorageProvider); - final credentialsLocalDataSource = CredentialsLocalDataSourceImpl( - secureStorage, - ); + // Use ref.read inside interceptor to get fresh data source each time + // This ensures we don't use a stale placeholder when SharedPreferences loads final authDio = ref.read(authDioProvider); - final tokenRefreshService = _getOrCreateTokenRefreshService( - authLocalDataSource, - credentialsLocalDataSource, - authDio, - ); - dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { + // Dynamically get the data source to ensure we have the latest + final authLocalDataSource = ref.read(authLocalDataSourceProvider); final token = await authLocalDataSource.getAccessToken(); + AppLogger.info('API Request - Token available: ${token != null}'); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } @@ -140,6 +146,17 @@ final apiDioProvider = Provider((ref) { // Handle 401 Unauthorized - attempt to refresh token if (error.response?.statusCode == 401) { AppLogger.info('Received 401, attempting to refresh token...'); + AppLogger.info('Request URL: ${error.requestOptions.uri}'); + + // Get fresh data sources for token refresh + final authLocalDataSource = ref.read(authLocalDataSourceProvider); + final credentialsDataSource = ref.read(credentialsLocalDataSourceProvider); + + final tokenRefreshService = _getOrCreateTokenRefreshService( + authLocalDataSource, + credentialsDataSource, + authDio, + ); final newToken = await tokenRefreshService.refreshToken(); @@ -186,11 +203,40 @@ final miniPlayerServiceProvider = Provider((ref) { }); // Auth Local Data Source (needed by apiDioProvider, so kept here) +// Uses SharedPreferences on macOS/iOS to avoid Keychain issues during development final authLocalDataSourceProvider = Provider((ref) { + if (Platform.isMacOS || Platform.isIOS) { + final prefsAsync = ref.watch(sharedPrefsProvider); + return prefsAsync.when( + data: (prefs) => AuthSharedPrefsDataSource(prefs), + loading: () => _PlaceholderAuthDataSource(), + error: (_, __) => _PlaceholderAuthDataSource(), + ); + } final secureStorage = ref.watch(secureStorageProvider); return AuthLocalDataSourceImpl(secureStorage); }); +/// Placeholder implementation while SharedPreferences is loading +class _PlaceholderAuthDataSource implements AuthLocalDataSource { + @override + Future clearTokens() async {} + @override + Future getAccessToken() async => null; + @override + Future getRefreshToken() async => null; + @override + Future getTokenExpiry() async => null; + @override + Future hasValidToken() async => false; + @override + Future saveTokens({ + required String accessToken, + required String refreshToken, + required DateTime expiry, + }) async {} +} + // Dio instance for LLM API calls (with proxy support) final llmDioProvider = Provider((ref) { final dio = Dio( diff --git a/lib/data/datasources/auth_shared_prefs_datasource.dart b/lib/data/datasources/auth_shared_prefs_datasource.dart new file mode 100644 index 0000000..aab8bf4 --- /dev/null +++ b/lib/data/datasources/auth_shared_prefs_datasource.dart @@ -0,0 +1,82 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import '../../core/constants/app_constants.dart'; +import '../../core/errors/exceptions.dart'; +import 'auth_local_datasource.dart'; + +/// Alternative AuthLocalDataSource implementation using SharedPreferences +/// Used on platforms where Keychain/SecureStorage is problematic (macOS/iOS dev) +class AuthSharedPrefsDataSource implements AuthLocalDataSource { + final SharedPreferences _prefs; + + AuthSharedPrefsDataSource(this._prefs); + + @override + Future getAccessToken() async { + try { + return _prefs.getString(AppConstants.accessTokenKey); + } catch (e) { + throw CacheException(message: 'Failed to read access token: $e'); + } + } + + @override + Future getRefreshToken() async { + try { + return _prefs.getString(AppConstants.refreshTokenKey); + } catch (e) { + throw CacheException(message: 'Failed to read refresh token: $e'); + } + } + + @override + Future getTokenExpiry() async { + try { + final expiryStr = _prefs.getString(AppConstants.tokenExpiryKey); + if (expiryStr == null) return null; + return DateTime.parse(expiryStr); + } catch (e) { + throw CacheException(message: 'Failed to read token expiry: $e'); + } + } + + @override + Future saveTokens({ + required String accessToken, + required String refreshToken, + required DateTime expiry, + }) async { + try { + await _prefs.setString(AppConstants.accessTokenKey, accessToken); + await _prefs.setString(AppConstants.refreshTokenKey, refreshToken); + await _prefs.setString(AppConstants.tokenExpiryKey, expiry.toIso8601String()); + } catch (e) { + throw CacheException(message: 'Failed to save tokens: $e'); + } + } + + @override + Future clearTokens() async { + try { + await _prefs.remove(AppConstants.accessTokenKey); + await _prefs.remove(AppConstants.refreshTokenKey); + await _prefs.remove(AppConstants.tokenExpiryKey); + } catch (e) { + throw CacheException(message: 'Failed to clear tokens: $e'); + } + } + + @override + Future hasValidToken() async { + try { + final accessToken = await getAccessToken(); + final expiry = await getTokenExpiry(); + + if (accessToken == null || expiry == null) return false; + + // Token is valid if it expires more than 5 minutes from now + return expiry.isAfter(DateTime.now().add(const Duration(minutes: 5))); + } catch (e) { + return false; + } + } +} diff --git a/lib/data/datasources/credentials_shared_prefs_datasource.dart b/lib/data/datasources/credentials_shared_prefs_datasource.dart new file mode 100644 index 0000000..bc6ad35 --- /dev/null +++ b/lib/data/datasources/credentials_shared_prefs_datasource.dart @@ -0,0 +1,191 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import '../../core/constants/app_constants.dart'; +import '../../domain/entities/proxy_settings.dart'; +import 'credentials_local_datasource.dart'; + +/// Alternative implementation using SharedPreferences +/// Used on platforms where Keychain/SecureStorage is problematic (macOS/iOS dev) +class CredentialsSharedPrefsDataSource implements CredentialsLocalDataSource { + final SharedPreferences _prefs; + + CredentialsSharedPrefsDataSource(this._prefs); + + // Spotify credentials + @override + Future getSpotifyClientId() async { + return _prefs.getString(AppConstants.spotifyClientIdKey); + } + + @override + Future getSpotifyClientSecret() async { + return _prefs.getString(AppConstants.spotifyClientSecretKey); + } + + @override + Future saveSpotifyCredentials({ + required String clientId, + required String clientSecret, + }) async { + await _prefs.setString(AppConstants.spotifyClientIdKey, clientId); + await _prefs.setString(AppConstants.spotifyClientSecretKey, clientSecret); + } + + @override + Future clearSpotifyCredentials() async { + await _prefs.remove(AppConstants.spotifyClientIdKey); + await _prefs.remove(AppConstants.spotifyClientSecretKey); + } + + @override + Future hasSpotifyCredentials() async { + final clientId = await getSpotifyClientId(); + final clientSecret = await getSpotifyClientSecret(); + return clientId != null && + clientId.isNotEmpty && + clientSecret != null && + clientSecret.isNotEmpty; + } + + // LLM credentials + @override + Future getLlmApiKey() async { + return _prefs.getString(AppConstants.llmApiKeyKey); + } + + @override + Future getLlmModel() async { + return _prefs.getString(AppConstants.llmModelKey); + } + + @override + Future getLlmBaseUrl() async { + return _prefs.getString(AppConstants.llmBaseUrlKey); + } + + @override + Future saveLlmCredentials({ + String apiKey = '', + required String model, + required String baseUrl, + }) async { + await _prefs.setString(AppConstants.llmApiKeyKey, apiKey); + await _prefs.setString(AppConstants.llmModelKey, model); + await _prefs.setString(AppConstants.llmBaseUrlKey, baseUrl); + } + + @override + Future clearLlmCredentials() async { + await _prefs.remove(AppConstants.llmApiKeyKey); + await _prefs.remove(AppConstants.llmModelKey); + await _prefs.remove(AppConstants.llmBaseUrlKey); + } + + @override + Future hasLlmConfig() async { + final model = await getLlmModel(); + final baseUrl = await getLlmBaseUrl(); + return model != null && + model.isNotEmpty && + baseUrl != null && + baseUrl.isNotEmpty; + } + + // Proxy configuration + @override + Future getAppProxySettings() async { + final enabled = _prefs.getString(AppConstants.proxyEnabledKey); + final typeStr = _prefs.getString(AppConstants.proxyTypeKey); + final host = _prefs.getString(AppConstants.proxyHostKey); + final portStr = _prefs.getString(AppConstants.proxyPortKey); + final username = _prefs.getString(AppConstants.proxyUsernameKey); + final password = _prefs.getString(AppConstants.proxyPasswordKey); + + return AppProxySettings( + enabled: enabled == 'true', + type: typeStr == 'socks5' ? AppProxyType.socks5 : AppProxyType.http, + host: host ?? '', + port: int.tryParse(portStr ?? '') ?? 0, + username: username, + password: password, + ); + } + + @override + Future saveAppProxySettings(AppProxySettings config) async { + await _prefs.setString( + AppConstants.proxyEnabledKey, + config.enabled.toString(), + ); + await _prefs.setString( + AppConstants.proxyTypeKey, + config.type == AppProxyType.socks5 ? 'socks5' : 'http', + ); + await _prefs.setString(AppConstants.proxyHostKey, config.host); + await _prefs.setString(AppConstants.proxyPortKey, config.port.toString()); + if (config.username != null) { + await _prefs.setString(AppConstants.proxyUsernameKey, config.username!); + } + if (config.password != null) { + await _prefs.setString(AppConstants.proxyPasswordKey, config.password!); + } + } + + @override + Future clearAppProxySettings() async { + await _prefs.remove(AppConstants.proxyEnabledKey); + await _prefs.remove(AppConstants.proxyTypeKey); + await _prefs.remove(AppConstants.proxyHostKey); + await _prefs.remove(AppConstants.proxyPortKey); + await _prefs.remove(AppConstants.proxyUsernameKey); + await _prefs.remove(AppConstants.proxyPasswordKey); + } + + // Feature flags + @override + Future getAudioFeaturesEnabled() async { + return _prefs.getString(AppConstants.audioFeaturesEnabledKey) == 'true'; + } + + @override + Future setAudioFeaturesEnabled(bool enabled) async { + await _prefs.setString( + AppConstants.audioFeaturesEnabledKey, + enabled.toString(), + ); + } + + @override + Future getGpuAccelerationEnabled() async { + return _prefs.getString(AppConstants.gpuAccelerationEnabledKey) == 'true'; + } + + @override + Future setGpuAccelerationEnabled(bool enabled) async { + await _prefs.setString( + AppConstants.gpuAccelerationEnabledKey, + enabled.toString(), + ); + } + + // GetSongBPM API + @override + Future getGetSongBpmApiKey() async { + return _prefs.getString(AppConstants.getSongBpmApiKeyKey); + } + + @override + Future setGetSongBpmApiKey(String apiKey) async { + await _prefs.setString(AppConstants.getSongBpmApiKeyKey, apiKey); + } + + @override + Future clearGetSongBpmApiKey() async { + await _prefs.remove(AppConstants.getSongBpmApiKeyKey); + } + + @override + Future hasGetSongBpmApiKey() async { + final apiKey = await getGetSongBpmApiKey(); + return apiKey != null && apiKey.isNotEmpty; + } +} diff --git a/lib/data/repositories/auth_repository_impl.dart b/lib/data/repositories/auth_repository_impl.dart index d147d83..f2f010d 100644 --- a/lib/data/repositories/auth_repository_impl.dart +++ b/lib/data/repositories/auth_repository_impl.dart @@ -81,38 +81,49 @@ class AuthRepositoryImpl implements AuthRepository { ) async { final credentials = base64Encode(utf8.encode('$clientId:$clientSecret')); - final response = await dio.post( - AppConfig.spotifyTokenUrl, - data: { - 'grant_type': 'authorization_code', - 'code': code, - 'redirect_uri': AppConfig.spotifyRedirectUri, - }, - options: Options( - headers: { - 'Authorization': 'Basic $credentials', - 'Content-Type': 'application/x-www-form-urlencoded', + AppLogger.info('Exchanging code for tokens...'); + AppLogger.info('Client ID: ${clientId.substring(0, 4)}...${clientId.substring(clientId.length - 4)}'); + AppLogger.info('Redirect URI: ${AppConfig.spotifyRedirectUri}'); + AppLogger.info('Code: ${code.substring(0, 10)}...'); + + try { + final response = await dio.post( + AppConfig.spotifyTokenUrl, + data: { + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': AppConfig.spotifyRedirectUri, }, - contentType: Headers.formUrlEncodedContentType, - ), - ); + options: Options( + headers: { + 'Authorization': 'Basic $credentials', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + contentType: Headers.formUrlEncodedContentType, + ), + ); - final data = response.data as Map; + final data = response.data as Map; - final accessToken = data['access_token'] as String; - final refreshToken = data['refresh_token'] as String; - final expiresIn = data['expires_in'] as int; - final expiry = DateTime.now().add(Duration(seconds: expiresIn)); + final accessToken = data['access_token'] as String; + final refreshToken = data['refresh_token'] as String; + final expiresIn = data['expires_in'] as int; + final expiry = DateTime.now().add(Duration(seconds: expiresIn)); - await localDataSource.saveTokens( - accessToken: accessToken, - refreshToken: refreshToken, - expiry: expiry, - ); + await localDataSource.saveTokens( + accessToken: accessToken, + refreshToken: refreshToken, + expiry: expiry, + ); - AppLogger.info('Tokens saved successfully'); + AppLogger.info('Tokens saved successfully'); - return data; + return data; + } on DioException catch (e) { + AppLogger.error('Token exchange failed with status: ${e.response?.statusCode}'); + AppLogger.error('Response data: ${e.response?.data}'); + rethrow; + } } @override diff --git a/lib/presentation/screens/setup_guide_screen.dart b/lib/presentation/screens/setup_guide_screen.dart index 3f4f704..908433a 100644 --- a/lib/presentation/screens/setup_guide_screen.dart +++ b/lib/presentation/screens/setup_guide_screen.dart @@ -413,6 +413,7 @@ class _SetupGuideScreenState extends ConsumerState { const SizedBox(height: 8), TextFormField( controller: _clientIdController, + style: const TextStyle(color: AppTheme.spotifyWhite), decoration: InputDecoration( hintText: l10n.enterClientId, prefixIcon: const Icon(Icons.key), @@ -443,6 +444,7 @@ class _SetupGuideScreenState extends ConsumerState { TextFormField( controller: _clientSecretController, obscureText: _obscureSecret, + style: const TextStyle(color: AppTheme.spotifyWhite), decoration: InputDecoration( hintText: l10n.enterClientSecret, prefixIcon: const Icon(Icons.lock), diff --git a/lib/presentation/themes/app_theme.dart b/lib/presentation/themes/app_theme.dart index 8d068b9..2521153 100644 --- a/lib/presentation/themes/app_theme.dart +++ b/lib/presentation/themes/app_theme.dart @@ -88,6 +88,7 @@ class AppTheme { filled: true, fillColor: spotifyDarkGray, hintStyle: const TextStyle(color: spotifyLightGray), + labelStyle: const TextStyle(color: spotifyLightGray), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, @@ -97,6 +98,11 @@ class AppTheme { borderSide: const BorderSide(color: spotifyGreen), ), ), + textSelectionTheme: const TextSelectionThemeData( + cursorColor: spotifyGreen, + selectionColor: Color(0x401DB954), + selectionHandleColor: spotifyGreen, + ), iconTheme: const IconThemeData(color: spotifyWhite), sliderTheme: const SliderThemeData( activeTrackColor: spotifyGreen, diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..2b24e5a --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,67 @@ +PODS: + - app_links (6.4.1): + - FlutterMacOS + - flutter_secure_storage_darwin (10.0.0): + - Flutter + - FlutterMacOS + - FlutterMacOS (1.0.0) + - screen_retriever_macos (0.0.1): + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - system_tray (0.0.1): + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - window_manager (0.5.0): + - FlutterMacOS + +DEPENDENCIES: + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) + - flutter_secure_storage_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_darwin/darwin`) + - FlutterMacOS (from `Flutter/ephemeral`) + - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) + - system_tray (from `Flutter/ephemeral/.symlinks/plugins/system_tray/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + +EXTERNAL SOURCES: + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos + flutter_secure_storage_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_darwin/darwin + FlutterMacOS: + :path: Flutter/ephemeral + screen_retriever_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin + system_tray: + :path: Flutter/ephemeral/.symlinks/plugins/system_tray/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + +SPEC CHECKSUMS: + app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f + flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + system_tray: 53f0cdb020c3fbee711d3fe45ae7ce730e033d2b + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + window_manager: b729e31d38fb04905235df9ea896128991cad99e + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 343f43a..d7cc54e 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 432DF61F143FE587805530C1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7872E42518F1D901A1C1997F /* Pods_Runner.framework */; }; + CF1154A7B1A128E2C95B7FBF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92C6A8044683D5B03EC50D11 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1A02A11FF2786C2BABC2E061 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* spotify_focus_someone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "spotify_focus_someone.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* spotify_focus_someone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = spotify_focus_someone.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +79,15 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 40A5919CEEA25305FD480901 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 5EC1F4FF9901D9493BAA6289 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 7872E42518F1D901A1C1997F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 92C6A8044683D5B03EC50D11 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + DCD3EA8915150CD94CCA1346 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E184780F3846821E99E79BE0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + EA12E4286190D5A7CF053645 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CF1154A7B1A128E2C95B7FBF /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 432DF61F143FE587805530C1 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 983AFEA246822E4B0C7349FE /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 983AFEA246822E4B0C7349FE /* Pods */ = { + isa = PBXGroup; + children = ( + DCD3EA8915150CD94CCA1346 /* Pods-Runner.debug.xcconfig */, + EA12E4286190D5A7CF053645 /* Pods-Runner.release.xcconfig */, + E184780F3846821E99E79BE0 /* Pods-Runner.profile.xcconfig */, + 5EC1F4FF9901D9493BAA6289 /* Pods-RunnerTests.debug.xcconfig */, + 40A5919CEEA25305FD480901 /* Pods-RunnerTests.release.xcconfig */, + 1A02A11FF2786C2BABC2E061 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 7872E42518F1D901A1C1997F /* Pods_Runner.framework */, + 92C6A8044683D5B03EC50D11 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + E8F48043C0F942A90F1A3EED /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 448EEE8ACBB3D9AD361422DA /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 80CBB0A6B7BABCEE177C77CD /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -329,6 +361,67 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 448EEE8ACBB3D9AD361422DA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 80CBB0A6B7BABCEE177C77CD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E8F48043C0F942A90F1A3EED /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 5EC1F4FF9901D9493BAA6289 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 40A5919CEEA25305FD480901 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 1A02A11FF2786C2BABC2E061 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index b3c1761..363a0a6 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -1,8 +1,20 @@ import Cocoa import FlutterMacOS +import app_links @main class AppDelegate: FlutterAppDelegate { + override func applicationWillFinishLaunching(_ notification: Notification) { + // Register for URL scheme events before the app finishes launching + NSAppleEventManager.shared().setEventHandler( + self, + andSelector: #selector(handleURLEvent(_:withReplyEvent:)), + forEventClass: AEEventClass(kInternetEventClass), + andEventID: AEEventID(kAEGetURL) + ) + super.applicationWillFinishLaunching(notification) + } + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } @@ -10,4 +22,15 @@ class AppDelegate: FlutterAppDelegate { override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } + + @objc func handleURLEvent(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) { + guard let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue else { + return + } + + print("AppDelegate: Received URL event: \(urlString)") + + // Forward to app_links plugin + AppLinks.shared.handleLink(link: urlString) + } } diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index dddb8a3..78c36cf 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -3,10 +3,12 @@ com.apple.security.app-sandbox - + com.apple.security.cs.allow-jit com.apple.security.network.server + com.apple.security.network.client + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 4789daa..a260130 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -20,6 +20,19 @@ $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.sfo.fullstop + CFBundleURLSchemes + + fullstop + + + LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 852fa1a..937ee6c 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,5 +4,11 @@ com.apple.security.app-sandbox + com.apple.security.network.client + + keychain-access-groups + + $(AppIdentifierPrefix)com.sfo.fullstop +