Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions lib/core/constants/app_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> chequeReminderDaysBefore = [1, 3, 7];

Expand Down
4 changes: 3 additions & 1 deletion lib/data/sync/sync_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand All @@ -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',
Expand All @@ -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),
Expand Down
71 changes: 36 additions & 35 deletions lib/data/sync/sync_tables_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ const List<SyncTable> 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;

Expand All @@ -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),
Expand All @@ -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),
],
Expand All @@ -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),
],
Expand All @@ -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),
],
Expand All @@ -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),
Expand All @@ -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),
],
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
],
Expand All @@ -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),
Expand All @@ -230,16 +231,16 @@ 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),
SyncColumn('paid_amount', _real),
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),
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
],
Expand All @@ -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),
Expand All @@ -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),
],
Expand All @@ -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),
],
Expand Down
7 changes: 6 additions & 1 deletion lib/features/settings/presentation/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down Expand Up @@ -468,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,
Expand Down
2 changes: 1 addition & 1 deletion lib/providers/auth_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 16 additions & 13 deletions lib/providers/eula_provider.dart
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
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<bool> {
static const _storage = FlutterSecureStorage();

@override
Future<bool> 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;
return (await _markerFile()).existsSync();
} catch (_) {
return false;
}
}

Future<void> 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());
} catch (_) {
return;
}
state = const AsyncData(true);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

static Future<File> _markerFile() async {
final dir = await getApplicationSupportDirectory();
return File('${dir.path}/.eula_accepted');
}
}

final eulaProvider = AsyncNotifierProvider<EulaNotifier, bool>(EulaNotifier.new);
4 changes: 3 additions & 1 deletion lib/providers/sync_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ class SyncState {

class SyncNotifier extends Notifier<SyncState> {
Timer? _timer;
static const _storage = FlutterSecureStorage();
static const _storage = FlutterSecureStorage(
mOptions: MacOsOptions(groupId: 'lk.getbms.bms'),
);

@override
SyncState build() {
Expand Down
4 changes: 4 additions & 0 deletions macos/Runner/DebugProfile.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
<false/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)lk.getbms.bms</string>
</array>
</dict>
</plist>
4 changes: 4 additions & 0 deletions macos/Runner/Release.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)lk.getbms.bms</string>
</array>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</dict>
</plist>
Loading