Merge branch 'login' of github.com:OpenBikeControl/bikecontrol into login

This commit is contained in:
Jonas Bark
2026-02-16 17:17:57 +01:00
4 changed files with 155 additions and 0 deletions

View File

@@ -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<LoginPage> {
core.supabase.auth.signOut();
},
),
if (kDebugMode && Platform.isWindows)
Button.secondary(
child: Text("Register"),
onPressed: () {
WindowsProtocolHandler().register("bikecontrol");
},
),
],
),
);

View File

@@ -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<AuthState> _authSubscription;
/// Check if the trial has expired
bool get isTrialExpired {
return !IAPManager.instance.isPurchased.value && hasTrialStarted && trialDaysRemaining <= 0;

View File

@@ -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<String> getArguments(List<String>? 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<String>? 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"';
}
}

View File

@@ -7,6 +7,7 @@
#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>
#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())