From 683b8d9bfffbf871f44a77af3565d2f1a39e01b7 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Tue, 23 Jun 2026 14:29:59 +0530 Subject: [PATCH 1/5] fix: use VARCHAR(36) for UUID columns in MySQL DDL generation MySQL rejects TEXT/LONGTEXT as primary key columns without a key length (error 1170). All id columns and UUID foreign keys (user_id, customer_id, product_id, etc.) are always 36-char UUIDs, so map them to VARCHAR(36) via a new SyncColumnType.uuid enum value instead of LONGTEXT. --- lib/data/sync/sync_table.dart | 4 +- lib/data/sync/sync_tables_registry.dart | 71 +++++++++++++------------ 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/lib/data/sync/sync_table.dart b/lib/data/sync/sync_table.dart index 70ff5d1..ec0be72 100644 --- a/lib/data/sync/sync_table.dart +++ b/lib/data/sync/sync_table.dart @@ -5,7 +5,7 @@ // passes them as-is. Pull (MySQL→SQLite) reads strings from the MySQL // client and casts them using SyncColumn.type. -enum SyncColumnType { text, integer, real } +enum SyncColumnType { uuid, text, integer, real } class SyncColumn { const SyncColumn(this.name, this.type, {this.nullable = false, this.primaryKey = false}); @@ -15,6 +15,7 @@ class SyncColumn { final bool primaryKey; String get mysqlType => switch (type) { + SyncColumnType.uuid => 'VARCHAR(36)', SyncColumnType.text => 'LONGTEXT', SyncColumnType.integer => 'BIGINT', SyncColumnType.real => 'DOUBLE', @@ -23,6 +24,7 @@ class SyncColumn { dynamic parseFromMysql(String? raw) { if (raw == null) return null; return switch (type) { + SyncColumnType.uuid => raw, SyncColumnType.text => raw, SyncColumnType.integer => int.tryParse(raw), SyncColumnType.real => double.tryParse(raw), diff --git a/lib/data/sync/sync_tables_registry.dart b/lib/data/sync/sync_tables_registry.dart index e741621..a5c3e59 100644 --- a/lib/data/sync/sync_tables_registry.dart +++ b/lib/data/sync/sync_tables_registry.dart @@ -29,8 +29,9 @@ const List kSyncTables = [ // Table descriptors // --------------------------------------------------------------------------- -const _pk = SyncColumn('id', SyncColumnType.text, primaryKey: true); +const _pk = SyncColumn('id', SyncColumnType.uuid, primaryKey: true); const _ts = SyncColumnType.integer; +const _uuid = SyncColumnType.uuid; const _txt = SyncColumnType.text; const _real = SyncColumnType.real; @@ -49,7 +50,7 @@ const _products = SyncTable( _pk, SyncColumn('name', _txt), SyncColumn('barcode', _txt, nullable: true), - SyncColumn('category_id', _txt, nullable: true), + SyncColumn('category_id', _uuid, nullable: true), SyncColumn('brand', _txt, nullable: true), SyncColumn('unit_type', _txt), SyncColumn('cost_price', _real), @@ -65,7 +66,7 @@ const _products = SyncTable( const _stock = SyncTable( sqliteName: 'stock', columns: [ - SyncColumn('product_id', _txt, primaryKey: true), + SyncColumn('product_id', _uuid, primaryKey: true), SyncColumn('qty', _real), SyncColumn('updated_at', _ts), ], @@ -89,12 +90,12 @@ const _customerPayments = SyncTable( sqliteName: 'customer_payments', columns: [ _pk, - SyncColumn('customer_id', _txt), + SyncColumn('customer_id', _uuid), SyncColumn('amount', _real), SyncColumn('method', _txt), SyncColumn('reference_no', _txt, nullable: true), SyncColumn('notes', _txt, nullable: true), - SyncColumn('user_id', _txt), + SyncColumn('user_id', _uuid), SyncColumn('created_at', _ts), SyncColumn('updated_at', _ts), ], @@ -118,13 +119,13 @@ const _supplierPayments = SyncTable( sqliteName: 'supplier_payments', columns: [ _pk, - SyncColumn('supplier_id', _txt), + SyncColumn('supplier_id', _uuid), SyncColumn('amount', _real), SyncColumn('method', _txt), - SyncColumn('cheque_id', _txt, nullable: true), + SyncColumn('cheque_id', _uuid, nullable: true), SyncColumn('reference_no', _txt, nullable: true), SyncColumn('notes', _txt, nullable: true), - SyncColumn('user_id', _txt), + SyncColumn('user_id', _uuid), SyncColumn('created_at', _ts), SyncColumn('updated_at', _ts), ], @@ -135,7 +136,7 @@ const _purchaseOrders = SyncTable( columns: [ _pk, SyncColumn('po_number', _txt), - SyncColumn('supplier_id', _txt), + SyncColumn('supplier_id', _uuid), SyncColumn('status', _txt), SyncColumn('notes', _txt, nullable: true), SyncColumn('total', _real), @@ -149,13 +150,13 @@ const _purchases = SyncTable( columns: [ _pk, SyncColumn('grn_number', _txt), - SyncColumn('supplier_id', _txt), - SyncColumn('po_id', _txt, nullable: true), + SyncColumn('supplier_id', _uuid), + SyncColumn('po_id', _uuid, nullable: true), SyncColumn('total', _real), SyncColumn('supplier_invoice_no', _txt, nullable: true), SyncColumn('supplier_invoice_amount', _real, nullable: true), SyncColumn('notes', _txt, nullable: true), - SyncColumn('user_id', _txt), + SyncColumn('user_id', _uuid), SyncColumn('created_at', _ts), SyncColumn('updated_at', _ts), ], @@ -165,8 +166,8 @@ const _purchaseItems = SyncTable( sqliteName: 'purchase_items', columns: [ _pk, - SyncColumn('purchase_id', _txt), - SyncColumn('product_id', _txt), + SyncColumn('purchase_id', _uuid), + SyncColumn('product_id', _uuid), SyncColumn('qty', _real), SyncColumn('cost_price', _real), SyncColumn('subtotal', _real), @@ -177,8 +178,8 @@ const _purchaseOrderItems = SyncTable( sqliteName: 'purchase_order_items', columns: [ _pk, - SyncColumn('po_id', _txt), - SyncColumn('product_id', _txt), + SyncColumn('po_id', _uuid), + SyncColumn('product_id', _uuid), SyncColumn('qty', _real), SyncColumn('cost_price', _real), SyncColumn('subtotal', _real), @@ -200,7 +201,7 @@ const _cheques = SyncTable( SyncColumn('bounce_date', _ts, nullable: true), SyncColumn('bounce_reason', _txt, nullable: true), SyncColumn('representation_count', _ts), - SyncColumn('user_id', _txt), + SyncColumn('user_id', _uuid), SyncColumn('created_at', _ts), SyncColumn('updated_at', _ts), ], @@ -217,8 +218,8 @@ const _pettyCash = SyncTable( SyncColumn('status', _txt), SyncColumn('receipt_path', _txt, nullable: true), SyncColumn('approval_notes', _txt, nullable: true), - SyncColumn('user_id', _txt), - SyncColumn('approved_by', _txt, nullable: true), + SyncColumn('user_id', _uuid), + SyncColumn('approved_by', _uuid, nullable: true), SyncColumn('approved_at', _ts, nullable: true), SyncColumn('created_at', _ts), SyncColumn('updated_at', _ts), @@ -230,7 +231,7 @@ const _invoices = SyncTable( columns: [ _pk, SyncColumn('invoice_no', _txt), - SyncColumn('customer_id', _txt, nullable: true), + SyncColumn('customer_id', _uuid, nullable: true), SyncColumn('subtotal', _real), SyncColumn('discount_amount', _real), SyncColumn('total', _real), @@ -238,8 +239,8 @@ const _invoices = SyncTable( SyncColumn('payment_type', _txt), SyncColumn('status', _txt), SyncColumn('void_reason', _txt, nullable: true), - SyncColumn('void_approved_by', _txt, nullable: true), - SyncColumn('user_id', _txt), + SyncColumn('void_approved_by', _uuid, nullable: true), + SyncColumn('user_id', _uuid), SyncColumn('created_at', _ts), SyncColumn('updated_at', _ts), SyncColumn('deleted_at', _ts, nullable: true), @@ -250,8 +251,8 @@ const _invoiceItems = SyncTable( sqliteName: 'invoice_items', columns: [ _pk, - SyncColumn('invoice_id', _txt), - SyncColumn('product_id', _txt), + SyncColumn('invoice_id', _uuid), + SyncColumn('product_id', _uuid), SyncColumn('product_name', _txt), SyncColumn('qty', _real), SyncColumn('unit_price', _real), @@ -266,11 +267,11 @@ const _noInvoiceSales = SyncTable( sqliteName: 'no_invoice_sales', columns: [ _pk, - SyncColumn('product_id', _txt), + SyncColumn('product_id', _uuid), SyncColumn('product_name', _txt), SyncColumn('qty', _real), SyncColumn('price', _real), - SyncColumn('user_id', _txt), + SyncColumn('user_id', _uuid), SyncColumn('notes', _txt, nullable: true), SyncColumn('created_at', _ts), SyncColumn('updated_at', _ts), @@ -281,12 +282,12 @@ const _salesReturns = SyncTable( sqliteName: 'sales_returns', columns: [ _pk, - SyncColumn('invoice_id', _txt), + SyncColumn('invoice_id', _uuid), SyncColumn('return_no', _txt), SyncColumn('type', _txt), SyncColumn('total_amount', _real), SyncColumn('reason', _txt, nullable: true), - SyncColumn('user_id', _txt), + SyncColumn('user_id', _uuid), SyncColumn('created_at', _ts), SyncColumn('updated_at', _ts), ], @@ -296,8 +297,8 @@ const _returnItems = SyncTable( sqliteName: 'return_items', columns: [ _pk, - SyncColumn('return_id', _txt), - SyncColumn('product_id', _txt), + SyncColumn('return_id', _uuid), + SyncColumn('product_id', _uuid), SyncColumn('product_name', _txt), SyncColumn('qty', _real), SyncColumn('unit_price', _real), @@ -311,11 +312,11 @@ const _auditLog = SyncTable( columns: [ _pk, SyncColumn('entity_type', _txt), - SyncColumn('entity_id', _txt), + SyncColumn('entity_id', _uuid), SyncColumn('action', _txt), SyncColumn('old_value', _txt, nullable: true), SyncColumn('new_value', _txt, nullable: true), - SyncColumn('user_id', _txt), + SyncColumn('user_id', _uuid), SyncColumn('user_name', _txt), SyncColumn('created_at', _ts), ], @@ -327,11 +328,11 @@ const _stockMovements = SyncTable( columns: [ _pk, SyncColumn('type', _txt), - SyncColumn('product_id', _txt), + SyncColumn('product_id', _uuid), SyncColumn('qty', _real), SyncColumn('reason', _txt, nullable: true), - SyncColumn('user_id', _txt), - SyncColumn('ref_id', _txt, nullable: true), + SyncColumn('user_id', _uuid), + SyncColumn('ref_id', _uuid, nullable: true), SyncColumn('ref_type', _txt, nullable: true), SyncColumn('created_at', _ts), ], From 3536ddf403639b288f4561f6f88562e8aad6ac2a Mon Sep 17 00:00:00 2001 From: iamvirul Date: Tue, 23 Jun 2026 14:33:37 +0530 Subject: [PATCH 2/5] fix: switch EULA storage to file, add keychain entitlements for macOS EULA acceptance now writes a plain marker file under applicationSupportDirectory instead of using flutter_secure_storage. The Keychain is overkill for non-sensitive preference data and was causing silent failures (and password prompts) on macOS. keychain-access-groups entitlement added to both Debug and Release entitlements so flutter_secure_storage (used for the license JWT) persists correctly across builds without prompting for the Mac login password each time. --- lib/core/constants/app_constants.dart | 3 --- lib/providers/eula_provider.dart | 31 +++++++++++--------------- macos/Runner/DebugProfile.entitlements | 4 ++++ macos/Runner/Release.entitlements | 4 ++++ 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart index 0e203f6..4f05ba7 100644 --- a/lib/core/constants/app_constants.dart +++ b/lib/core/constants/app_constants.dart @@ -14,9 +14,6 @@ abstract final class AppConstants { // Low stock fallback threshold (products carry their own reorder_level) static const int defaultReorderLevel = 10; - // EULA acceptance (stored as ISO-8601 acceptance date) - static const String eulaStorageKey = 'bms.eula.accepted'; - // Cheque reminder schedule (days before due date) static const List chequeReminderDaysBefore = [1, 3, 7]; diff --git a/lib/providers/eula_provider.dart b/lib/providers/eula_provider.dart index 479a290..0b01079 100644 --- a/lib/providers/eula_provider.dart +++ b/lib/providers/eula_provider.dart @@ -1,33 +1,28 @@ -import 'package:bms/core/constants/app_constants.dart'; +import 'dart:io'; + import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:path_provider/path_provider.dart'; +// EULA acceptance is not sensitive data — store as a plain marker file rather +// than in the Keychain to avoid macOS password prompts on every launch. class EulaNotifier extends AsyncNotifier { - static const _storage = FlutterSecureStorage(); - @override Future build() async { - // On web we skip the EULA gate (demo/preview builds). if (kIsWeb) return true; - - try { - final value = await _storage.read(key: AppConstants.eulaStorageKey); - return value != null; - } catch (_) { - return false; - } + return (await _markerFile()).existsSync(); } Future accept() async { - try { - await _storage.write( - key: AppConstants.eulaStorageKey, - value: DateTime.now().toUtc().toIso8601String(), - ); - } catch (_) {} + final file = await _markerFile(); + await file.writeAsString(DateTime.now().toUtc().toIso8601String()); state = const AsyncData(true); } + + static Future _markerFile() async { + final dir = await getApplicationSupportDirectory(); + return File('${dir.path}/.eula_accepted'); + } } final eulaProvider = AsyncNotifierProvider(EulaNotifier.new); diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index 8c9b91c..918b0f2 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -6,5 +6,9 @@ com.apple.security.cs.allow-jit + keychain-access-groups + + $(AppIdentifierPrefix)lk.getbms.bms + diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index e89b7f3..3f6225a 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,5 +4,9 @@ com.apple.security.app-sandbox + keychain-access-groups + + $(AppIdentifierPrefix)lk.getbms.bms + From e1bf787b346582a2207c497c19b366e8faad3d44 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Tue, 23 Jun 2026 14:26:30 +0530 Subject: [PATCH 3/5] fix: add 100ms delay before file picker to prevent macOS blackout NSOpenPanel captures window focus before Flutter can repaint after a dialog closes, leaving the window black. A one-frame delay lets the renderer settle before the native panel opens. --- lib/features/settings/presentation/settings_screen.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index dac38c5..3123d07 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -195,6 +195,10 @@ class SettingsScreen extends ConsumerWidget { ), ); if (confirmed != true || !context.mounted) return; + // Give Flutter one frame to repaint before NSOpenPanel captures focus, + // otherwise the macOS window goes black. + await Future.delayed(const Duration(milliseconds: 100)); + if (!context.mounted) return; final msg = await ref.read(settingsActionsProvider).importDatabaseFromJson(); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); From 215314496bd933e6d29240b7950ad45606e0b0c3 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Tue, 23 Jun 2026 14:35:54 +0530 Subject: [PATCH 4/5] fix: unify backend card background color with text field fill Backend card used surfaceVariant (#EEF2F7) while TextField widgets filled white (surface), creating a visible colour mismatch inside the same card. Switch to surface + border to match the field fill. --- lib/features/settings/presentation/settings_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index 3123d07..acb4f8e 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -472,8 +472,9 @@ class _DbConnectionTileState extends ConsumerState<_DbConnectionTile> { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.surfaceVariant, + color: AppColors.surface, borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, From 407c0fd8cd6ceae49589bfb3bca19d810e7eb905 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Tue, 23 Jun 2026 20:52:16 +0530 Subject: [PATCH 5/5] fix: apply CodeRabbit review findings on eula and keychain options - Replace em-dash with hyphen in eula_provider.dart comment - Add try/catch to EulaNotifier.build() and accept(); accept() no longer updates state when the file write fails - Add groupId: 'lk.getbms.bms' to MacOsOptions in auth_provider.dart to match the keychain-access-groups entitlement - Add MacOsOptions(groupId: 'lk.getbms.bms') to SyncNotifier._storage so sync provider uses the same keychain access group Skipped: ALTER TABLE migration for UUID columns - CREATE TABLE with a LONGTEXT primary key always threw error 1170, so no stale tables exist. --- lib/providers/auth_provider.dart | 2 +- lib/providers/eula_provider.dart | 16 ++++++++++++---- lib/providers/sync_provider.dart | 4 +++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index 94d3871..831d59d 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -11,7 +11,7 @@ part 'auth_provider.g.dart'; FlutterSecureStorage secureStorage(Ref ref) => const FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), - mOptions: MacOsOptions(useDataProtectionKeyChain: false), + mOptions: MacOsOptions(useDataProtectionKeyChain: false, groupId: 'lk.getbms.bms'), ); @Riverpod(keepAlive: true) diff --git a/lib/providers/eula_provider.dart b/lib/providers/eula_provider.dart index 0b01079..f304d14 100644 --- a/lib/providers/eula_provider.dart +++ b/lib/providers/eula_provider.dart @@ -4,18 +4,26 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; -// EULA acceptance is not sensitive data — store as a plain marker file rather +// EULA acceptance is not sensitive data - store as a plain marker file rather // than in the Keychain to avoid macOS password prompts on every launch. class EulaNotifier extends AsyncNotifier { @override Future build() async { if (kIsWeb) return true; - return (await _markerFile()).existsSync(); + try { + return (await _markerFile()).existsSync(); + } catch (_) { + return false; + } } Future accept() async { - final file = await _markerFile(); - await file.writeAsString(DateTime.now().toUtc().toIso8601String()); + try { + final file = await _markerFile(); + await file.writeAsString(DateTime.now().toUtc().toIso8601String()); + } catch (_) { + return; + } state = const AsyncData(true); } diff --git a/lib/providers/sync_provider.dart b/lib/providers/sync_provider.dart index 8484cb7..522277c 100644 --- a/lib/providers/sync_provider.dart +++ b/lib/providers/sync_provider.dart @@ -57,7 +57,9 @@ class SyncState { class SyncNotifier extends Notifier { Timer? _timer; - static const _storage = FlutterSecureStorage(); + static const _storage = FlutterSecureStorage( + mOptions: MacOsOptions(groupId: 'lk.getbms.bms'), + ); @override SyncState build() {