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
+