From dcd11e6c4baa6e7c4e4a18cc2406688d6041750e Mon Sep 17 00:00:00 2001 From: "jonas.bark@gmx.de" Date: Mon, 16 Feb 2026 17:14:38 +0100 Subject: [PATCH] Windows login --- lib/pages/login.dart | 8 +++ lib/utils/iap/windows_iap_service.dart | 31 ++++++++++++ lib/utils/requirements/windows.dart | 70 ++++++++++++++++++++++++++ windows/runner/main.cpp | 46 +++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 lib/utils/requirements/windows.dart diff --git a/lib/pages/login.dart b/lib/pages/login.dart index a91026d..b7ee8cb 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:bike_control/utils/requirements/windows.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:google_sign_in/google_sign_in.dart'; @@ -43,6 +44,13 @@ class _LoginPageState extends State { core.supabase.auth.signOut(); }, ), + if (kDebugMode && Platform.isWindows) + Button.secondary( + child: Text("Register"), + onPressed: () { + WindowsProtocolHandler().register("bikecontrol"); + }, + ), ], ), ); diff --git a/lib/utils/iap/windows_iap_service.dart b/lib/utils/iap/windows_iap_service.dart index 6893fb1..f01f780 100644 --- a/lib/utils/iap/windows_iap_service.dart +++ b/lib/utils/iap/windows_iap_service.dart @@ -8,6 +8,9 @@ import 'package:bike_control/utils/windows_store_environment.dart'; import 'package:bike_control/widgets/ui/toast.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:gotrue/src/types/auth_state.dart'; +import 'package:prop/prop.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:windows_iap/windows_iap.dart'; /// Windows-specific IAP service @@ -44,6 +47,32 @@ class WindowsIAPService { _lastCommandDate = await _prefs.read(key: _lastCommandDateKey); _dailyCommandCount = int.tryParse(await _prefs.read(key: _dailyCommandCountKey) ?? '0'); _isInitialized = true; + + + _authSubscription = core.supabase.auth.onAuthStateChange.listen((data) { + final AuthChangeEvent event = data.event; + final Session? session = data.session; + + Logger.info('event: $event, session: ${session?.user.id} via ${session?.user.email}'); + + switch (event) { + case AuthChangeEvent.initialSession: + case AuthChangeEvent.signedIn: + // handle signed in + case AuthChangeEvent.signedOut: + // handle signed out + case AuthChangeEvent.passwordRecovery: + // handle password recovery + case AuthChangeEvent.tokenRefreshed: + // handle token refreshed + case AuthChangeEvent.userUpdated: + // handle user updated + case AuthChangeEvent.userDeleted: + // handle user deleted + case AuthChangeEvent.mfaChallengeVerified: + // handle mfa challenge verified + } + }); } catch (e, s) { recordError(e, s, context: 'Initializing'); debugPrint('Failed to initialize Windows IAP: $e'); @@ -108,6 +137,8 @@ class WindowsIAPService { /// Get the number of days remaining in the trial int trialDaysRemaining = 0; + late final StreamSubscription _authSubscription; + /// Check if the trial has expired bool get isTrialExpired { return !IAPManager.instance.isPurchased.value && hasTrialStarted && trialDaysRemaining <= 0; diff --git a/lib/utils/requirements/windows.dart b/lib/utils/requirements/windows.dart new file mode 100644 index 0000000..1670119 --- /dev/null +++ b/lib/utils/requirements/windows.dart @@ -0,0 +1,70 @@ +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; +import 'package:win32/win32.dart'; + +const _hive = HKEY_CURRENT_USER; + +class WindowsProtocolHandler { + List getArguments(List? arguments) { + if (arguments == null) return ['%s']; + + if (arguments.isEmpty && !arguments.any((e) => e.contains('%s'))) { + throw ArgumentError('arguments must contain at least 1 instance of "%s"'); + } + + return arguments; + } + + void register(String scheme, {String? executable, List? arguments}) { + if (defaultTargetPlatform != TargetPlatform.windows) return; + + final prefix = _regPrefix(scheme); + final capitalized = scheme[0].toUpperCase() + scheme.substring(1); + final args = getArguments(arguments).map((a) => _sanitize(a)); + final cmd = '${executable ?? Platform.resolvedExecutable} ${args.join(' ')}'; + + _regCreateStringKey(_hive, prefix, '', 'URL:$capitalized'); + _regCreateStringKey(_hive, prefix, 'URL Protocol', ''); + _regCreateStringKey(_hive, '$prefix\\shell\\open\\command', '', cmd); + } + + void unregister(String scheme) { + if (defaultTargetPlatform != TargetPlatform.windows) return; + + final txtKey = TEXT(_regPrefix(scheme)); + try { + RegDeleteTree(HKEY_CURRENT_USER, txtKey); + } finally { + free(txtKey); + } + } + + String _regPrefix(String scheme) => 'SOFTWARE\\Classes\\$scheme'; + + int _regCreateStringKey(int hKey, String key, String valueName, String data) { + final txtKey = TEXT(key); + final txtValue = TEXT(valueName); + final txtData = TEXT(data); + try { + return RegSetKeyValue( + hKey, + txtKey, + txtValue, + REG_SZ, + txtData, + txtData.length * 2 + 2, + ); + } finally { + free(txtKey); + free(txtValue); + free(txtData); + } + } + + String _sanitize(String value) { + value = value.replaceAll(r'%s', '%1').replaceAll(r'"', '\\"'); + return '"$value"'; + } +} diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 326da88..13faa7e 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -7,6 +7,7 @@ #include #include +#include "app_links/app_links_plugin_c_api.h" namespace { @@ -44,9 +45,54 @@ namespace } // namespace +bool SendAppLinkToInstance(const std::wstring &title) +{ + // Find our exact window + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str()); + + if (hwnd) + { + // Dispatch new link to current window + SendAppLink(hwnd); + + // (Optional) Restore our window to front in same state + WINDOWPLACEMENT place = {sizeof(WINDOWPLACEMENT)}; + GetWindowPlacement(hwnd, &place); + + switch (place.showCmd) + { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hwnd); + // END (Optional) Restore + + // Window has been found, don't create another one. + return true; + } + + return false; +} + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + // Replace "example" with the generated title found as parameter of `window.Create` in this file. + // You may ignore the result if you need to create another window. + if (SendAppLinkToInstance(L"BikeControl")) + { + return EXIT_SUCCESS; + } + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())