diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 4281bae..c503614 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -1,3 +1,6 @@
+import java.util.Properties
+import java.io.FileInputStream
+
plugins {
id("com.android.application")
id("kotlin-android")
@@ -5,8 +8,15 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
+// Load key.properties
+val keystorePropertiesFile = rootProject.file("key.properties")
+val keystoreProperties = Properties()
+if (keystorePropertiesFile.exists()) {
+ keystoreProperties.load(FileInputStream(keystorePropertiesFile))
+}
+
android {
- namespace = "com.example.payfrit_app"
+ namespace = "com.payfrit.app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -20,23 +30,25 @@ android {
}
defaultConfig {
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId = "com.example.payfrit_app"
- // You can update the following values to match your application needs.
- // For more information, see: https://flutter.dev/to/review-gradle-config.
+ applicationId = "com.payfrit.app"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
- versionCode = flutter.versionCode
- versionName = flutter.versionName
+ versionCode = 1
+ versionName = "1.0.0"
+ }
+
+ signingConfigs {
+ create("release") {
+ keyAlias = keystoreProperties["keyAlias"] as String?
+ keyPassword = keystoreProperties["keyPassword"] as String?
+ storeFile = keystoreProperties["storeFile"]?.let { file(it) }
+ storePassword = keystoreProperties["storePassword"] as String?
+ }
}
buildTypes {
release {
- // TODO: Add your own signing config for the release build.
- // Signing with the debug keys for now, so `flutter run --release` works.
- signingConfig = signingConfigs.getByName("debug")
-
- // Disable minification and shrinking to preserve beacon library
+ signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c9ecef0..d0c77f5 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -19,7 +19,7 @@
-
-
-
-
+
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..0805617
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..030fdbc
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..6d20d4c
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
index 304732f..5f7a7df 100644
--- a/android/app/src/main/res/drawable/launch_background.xml
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -1,12 +1,5 @@
-
-
-
-
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..5f349f7
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index db77bb4..dc3d5e2 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 17987b7..1ec377a 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 09d4391..cde8c51 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d5f1c8d..b5d59ad 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4d6372e..69270a9 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..beab31f
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #000000
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index cb1ef88..6d2e15d 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -1,7 +1,7 @@
-
-
diff --git a/generate_icon.html b/generate_icon.html
new file mode 100644
index 0000000..3adb9c4
--- /dev/null
+++ b/generate_icon.html
@@ -0,0 +1,79 @@
+
+
+
+ Payfrit Icon Generator
+
+
+
+ 1024x1024 Icon Preview:
+
+
+
+
+
+
diff --git a/icon.jpg b/icon.jpg
new file mode 100644
index 0000000..d7c3edf
Binary files /dev/null and b/icon.jpg differ
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..50032ed
Binary files /dev/null and b/icon.png differ
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 91d2ecc..d4110ee 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -427,7 +427,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index dc9ada4..a66402d 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 7353c41..f69845d 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index 797d452..1b3f6ad 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index 6ed2d93..5c5aa9f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 4cd7b00..7804380 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index fe73094..7924420 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index 321773c..d750536 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index 797d452..1b3f6ad 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 502f463..573c84d 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index 0ec3034..1d7facb 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
new file mode 100644
index 0000000..8364506
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
new file mode 100644
index 0000000..256619e
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
new file mode 100644
index 0000000..997b916
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
new file mode 100644
index 0000000..44c75ff
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index 0ec3034..1d7facb 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index e9f5fea..e41c71e 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
new file mode 100644
index 0000000..970d5a5
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
new file mode 100644
index 0000000..845665b
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 84ac32a..8caca08 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index 8953cba..7f89976 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 0467bf1..fabcfc5 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/lib/models/cart.dart b/lib/models/cart.dart
index 7361513..19eafa4 100644
--- a/lib/models/cart.dart
+++ b/lib/models/cart.dart
@@ -97,13 +97,48 @@ class Cart {
}
double get subtotal {
- return lineItems
- .where((item) => !item.isDeleted && item.parentOrderLineItemId == 0)
- .fold(0.0, (sum, item) => sum + (item.price * item.quantity));
+ // Sum all non-deleted line items (root items and modifiers)
+ // Root items have their base price, modifiers have their add-on prices
+ // Modifier prices are multiplied by the root item's quantity
+ double total = 0.0;
+
+ // First, get all root items and their prices
+ final rootItems = lineItems.where((item) => !item.isDeleted && item.parentOrderLineItemId == 0);
+ for (final rootItem in rootItems) {
+ total += rootItem.price * rootItem.quantity;
+
+ // Add all modifier prices for this root item (recursively)
+ total += _sumModifierPrices(rootItem.orderLineItemId, rootItem.quantity);
+ }
+
+ return total;
+ }
+
+ /// Recursively sum modifier prices for a parent line item
+ double _sumModifierPrices(int parentOrderLineItemId, int rootQuantity) {
+ double total = 0.0;
+ final children = lineItems.where(
+ (item) => !item.isDeleted && item.parentOrderLineItemId == parentOrderLineItemId
+ );
+
+ for (final child in children) {
+ // Modifier price is multiplied by root item quantity
+ total += child.price * rootQuantity;
+ // Recursively add grandchildren modifier prices
+ total += _sumModifierPrices(child.orderLineItemId, rootQuantity);
+ }
+
+ return total;
}
// Only include delivery fee for delivery orders (orderTypeId == 3)
- double get total => subtotal + (orderTypeId == 3 ? deliveryFee : 0);
+ // Sales tax rate (8.25% for California - can be made configurable per business later)
+ static const double taxRate = 0.0825;
+
+ // Calculate sales tax on subtotal
+ double get tax => subtotal * taxRate;
+
+ double get total => subtotal + tax + (orderTypeId == 3 ? deliveryFee : 0);
int get itemCount {
return lineItems
diff --git a/lib/screens/beacon_scan_screen.dart b/lib/screens/beacon_scan_screen.dart
index c4d8e00..385aaab 100644
--- a/lib/screens/beacon_scan_screen.dart
+++ b/lib/screens/beacon_scan_screen.dart
@@ -114,7 +114,7 @@ class _BeaconScanScreenState extends State with SingleTickerPr
// Brief delay to let Bluetooth subsystem fully initialize
// Without this, the first scan cycle may complete immediately with no results
- await Future.delayed(const Duration(milliseconds: 500));
+ await Future.delayed(const Duration(milliseconds: 1500));
// Create regions for all known UUIDs
final regions = _uuidToBeaconId.keys.map((uuid) {
@@ -131,10 +131,11 @@ class _BeaconScanScreenState extends State with SingleTickerPr
return;
}
- // Perform 5 scans of 2 seconds each to increase chance of detecting all beacons
- print('[BeaconScan] đ Starting 5 scan cycles of 2 seconds each');
+ // Perform up to 5 scans of 2 seconds each, but exit early if readings are consistent
+ print('[BeaconScan] đ Starting scan cycles (max 5 x 2 seconds, early exit if stable)');
- for (int scanCycle = 1; scanCycle <= 5; scanCycle++) {
+ bool earlyExit = false;
+ for (int scanCycle = 1; scanCycle <= 3; scanCycle++) {
// Update status message for each cycle
if (mounted) {
setState(() => _status = _scanMessages[scanCycle - 1]);
@@ -165,16 +166,25 @@ class _BeaconScanScreenState extends State with SingleTickerPr
});
// Wait 2 seconds for this scan cycle to collect beacon data
- await Future.delayed(const Duration(seconds: 2));
+ await Future.delayed(const Duration(milliseconds: 1500));
await subscription.cancel();
+ // After 3 cycles, check if we can exit early
+ if (scanCycle >= 2 && _beaconRssiSamples.isNotEmpty) {
+ if (_canExitEarly()) {
+ print('[BeaconScan] ⥠Early exit after $scanCycle cycles - readings are stable!');
+ earlyExit = true;
+ break;
+ }
+ }
+
// Short pause between scan cycles
- if (scanCycle < 5) {
- await Future.delayed(const Duration(milliseconds: 200));
+ if (scanCycle < 3) {
+ await Future.delayed(const Duration(milliseconds: 1500));
}
}
- print('[BeaconScan] âī¸ All scan cycles complete');
+ print('[BeaconScan] âī¸ Scan complete${earlyExit ? ' (early exit)' : ''}');
if (!mounted) return;
@@ -291,11 +301,65 @@ class _BeaconScanScreenState extends State with SingleTickerPr
}
}
+ /// Check if we can exit early based on stable readings
+ /// Returns true if all detected beacons have consistent RSSI across scans
+ bool _canExitEarly() {
+ if (_beaconRssiSamples.isEmpty) return false;
+
+ // Need at least one beacon with 3+ readings
+ bool hasEnoughSamples = _beaconRssiSamples.values.any((samples) => samples.length >= 2);
+ if (!hasEnoughSamples) return false;
+
+ // Check if there's a clear winner (one beacon significantly stronger than others)
+ // or if all beacons have low variance in their readings
+ for (final entry in _beaconRssiSamples.entries) {
+ final samples = entry.value;
+ if (samples.length < 3) continue;
+
+ // Calculate variance
+ final avg = samples.reduce((a, b) => a + b) / samples.length;
+ final variance = samples.map((r) => (r - avg) * (r - avg)).reduce((a, b) => a + b) / samples.length;
+
+ // If variance is high (readings fluctuating a lot), keep scanning
+ // Variance > 50 means RSSI is jumping around too much
+ if (variance > 50) {
+ print('[BeaconScan] âŗ Beacon ${_uuidToBeaconId[entry.key]} has high variance (${variance.toStringAsFixed(1)}), continuing scan');
+ return false;
+ }
+ }
+
+ // If we have multiple beacons, check if there's a clear strongest one
+ if (_beaconRssiSamples.length > 1) {
+ final avgRssis = {};
+ for (final entry in _beaconRssiSamples.entries) {
+ final samples = entry.value;
+ if (samples.isNotEmpty) {
+ avgRssis[entry.key] = samples.reduce((a, b) => a + b) / samples.length;
+ }
+ }
+
+ // Sort by RSSI descending
+ final sorted = avgRssis.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
+
+ // If top two beacons are within 5 dB, keep scanning for more clarity
+ if (sorted.length >= 2) {
+ final diff = sorted[0].value - sorted[1].value;
+ if (diff < 5) {
+ print('[BeaconScan] âŗ Top 2 beacons are close (diff=${diff.toStringAsFixed(1)} dB), continuing scan');
+ return false;
+ }
+ }
+ }
+
+ print('[BeaconScan] â Readings are stable, can exit early');
+ return true;
+ }
+
BeaconScore? _findBestBeacon(Map scores) {
if (scores.isEmpty) return null;
// Filter beacons that meet minimum requirements
- const minDetections = 3; // Must be seen at least 3 times
+ const minDetections = 2; // Must be seen at least 3 times
const minRssi = -85; // Minimum average RSSI (beacons further than ~10m will be weaker)
final qualified = scores.values.where((score) {
@@ -386,6 +450,21 @@ class _BeaconScanScreenState extends State with SingleTickerPr
child: const Text('Open Settings'),
),
],
+
+ // Always show manual selection option during or after scan
+ if (_permissionsGranted) ...[
+ const SizedBox(height: 32),
+ TextButton(
+ onPressed: _navigateToRestaurantSelect,
+ child: const Text(
+ 'Select Restaurant Manually',
+ style: TextStyle(
+ color: Colors.white70,
+ fontSize: 14,
+ ),
+ ),
+ ),
+ ],
],
),
),
diff --git a/lib/screens/cart_view_screen.dart b/lib/screens/cart_view_screen.dart
index 99580a5..d5caf0f 100644
--- a/lib/screens/cart_view_screen.dart
+++ b/lib/screens/cart_view_screen.dart
@@ -315,6 +315,9 @@ class _CartViewScreenState extends State {
// Find ALL modifiers (recursively) and build breadcrumb paths for leaf items only
final modifierPaths = _buildModifierPaths(rootItem.orderLineItemId);
+ // Calculate total price for this line item (root + all modifiers)
+ final lineItemTotal = _calculateLineItemTotal(rootItem);
+
print('[Cart] Found ${modifierPaths.length} modifier paths for this root item');
return Card(
@@ -367,7 +370,7 @@ class _CartViewScreenState extends State {
),
const Spacer(),
Text(
- "\$${rootItem.price.toStringAsFixed(2)}",
+ "\$${lineItemTotal.toStringAsFixed(2)}",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -381,7 +384,32 @@ class _CartViewScreenState extends State {
);
}
+ /// Calculate the total price for a root item including all its modifiers
+ double _calculateLineItemTotal(OrderLineItem rootItem) {
+ double total = rootItem.price * rootItem.quantity;
+ total += _sumModifierPrices(rootItem.orderLineItemId, rootItem.quantity);
+ return total;
+ }
+
+ /// Recursively sum modifier prices for a parent line item
+ double _sumModifierPrices(int parentOrderLineItemId, int rootQuantity) {
+ double total = 0.0;
+ final children = _cart!.lineItems.where(
+ (item) => !item.isDeleted && item.parentOrderLineItemId == parentOrderLineItemId
+ );
+
+ for (final child in children) {
+ // Modifier price is multiplied by root item quantity
+ total += child.price * rootQuantity;
+ // Recursively add grandchildren modifier prices
+ total += _sumModifierPrices(child.orderLineItemId, rootQuantity);
+ }
+
+ return total;
+ }
+
/// Build breadcrumb paths for all leaf modifiers
+ /// Excludes default items - they don't need to be shown in the cart
List _buildModifierPaths(int rootOrderLineItemId) {
final paths = [];
@@ -394,13 +422,19 @@ class _CartViewScreenState extends State {
// Recursively collect leaf items with their paths
void collectLeafPaths(OrderLineItem item, List currentPath) {
+ final menuItem = _menuItemsById[item.itemId];
+
+ // Skip default items - they don't need to be repeated in the cart
+ if (menuItem?.isCheckedByDefault == true) {
+ return;
+ }
+
final children = _cart!.lineItems
.where((child) =>
child.parentOrderLineItemId == item.orderLineItemId &&
!child.isDeleted)
.toList();
- final menuItem = _menuItemsById[item.itemId];
final itemName = menuItem?.name ?? "Item #${item.itemId}";
if (children.isEmpty) {
@@ -486,6 +520,21 @@ class _CartViewScreenState extends State {
),
],
),
+ // Sales tax
+ const SizedBox(height: 8),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ "Tax (8.25%)",
+ style: TextStyle(fontSize: 16),
+ ),
+ Text(
+ "\$${_cart!.tax.toStringAsFixed(2)}",
+ style: const TextStyle(fontSize: 16),
+ ),
+ ],
+ ),
// Only show delivery fee for delivery orders (OrderTypeID = 3)
if (_cart!.deliveryFee > 0 && _cart!.orderTypeId == 3) ...[
const SizedBox(height: 8),
diff --git a/lib/screens/menu_browse_screen.dart b/lib/screens/menu_browse_screen.dart
index 5324792..8648fb2 100644
--- a/lib/screens/menu_browse_screen.dart
+++ b/lib/screens/menu_browse_screen.dart
@@ -80,16 +80,40 @@ class _MenuBrowseScreenState extends State {
_itemsByCategory.clear();
_itemsByParent.clear();
+ print('[MenuBrowse] _organizeItems: ${_allItems.length} total items');
+
+ // First pass: identify category items (root items where itemId == categoryId)
+ // These are the category headers themselves, NOT menu items
+ final categoryItemIds = {};
+ for (final item in _allItems) {
+ if (item.isRootItem && item.itemId == item.categoryId) {
+ categoryItemIds.add(item.itemId);
+ // Just register the category key (empty list for now)
+ _itemsByCategory.putIfAbsent(item.itemId, () => []);
+ print('[MenuBrowse] Category found: ${item.name} (ID=${item.itemId})');
+ }
+ }
+
+ print('[MenuBrowse] Found ${categoryItemIds.length} categories: $categoryItemIds');
+
+ // Second pass: organize menu items and modifiers
for (final item in _allItems) {
// Skip inactive items
if (!item.isActive) continue;
- if (item.isRootItem) {
- _itemsByCategory.putIfAbsent(item.categoryId, () => []).add(item);
+ // Skip category header items (they're not menu items to display)
+ if (categoryItemIds.contains(item.itemId)) continue;
+
+ // Check if parent is a category
+ if (categoryItemIds.contains(item.parentItemId)) {
+ // Direct child of a category = menu item (goes in _itemsByCategory)
+ _itemsByCategory.putIfAbsent(item.parentItemId, () => []).add(item);
+ print('[MenuBrowse] Menu item: ${item.name} -> category ${item.parentItemId}');
} else {
- // Prevent an item from being its own child
+ // Child of a menu item = modifier (goes in _itemsByParent)
if (item.itemId != item.parentItemId) {
_itemsByParent.putIfAbsent(item.parentItemId, () => []).add(item);
+ print('[MenuBrowse] Modifier: ${item.name} -> parent ${item.parentItemId}');
}
}
}
@@ -103,6 +127,11 @@ class _MenuBrowseScreenState extends State {
for (final list in _itemsByParent.values) {
list.sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
}
+
+ // Debug: print final counts
+ for (final entry in _itemsByCategory.entries) {
+ print('[MenuBrowse] Category ${entry.key}: ${entry.value.length} items');
+ }
}
List _getUniqueCategoryIds() {
@@ -386,6 +415,56 @@ class _MenuBrowseScreenState extends State {
);
}
+ /// Builds category background - tries image first, falls back to styled text
+ Widget _buildCategoryBackground(int categoryId, String categoryName) {
+ return Image.network(
+ "$_imageBaseUrl/categories/$categoryId.png",
+ fit: BoxFit.cover,
+ semanticLabel: categoryName,
+ errorBuilder: (context, error, stackTrace) {
+ return Image.network(
+ "$_imageBaseUrl/categories/$categoryId.jpg",
+ fit: BoxFit.cover,
+ semanticLabel: categoryName,
+ errorBuilder: (context, error, stackTrace) {
+ // No image - show large styled category name
+ return Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ Theme.of(context).colorScheme.primary,
+ Theme.of(context).colorScheme.tertiary,
+ ],
+ ),
+ ),
+ alignment: Alignment.center,
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: Text(
+ categoryName,
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ fontSize: 28,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ letterSpacing: 1.2,
+ shadows: [
+ Shadow(
+ offset: Offset(2, 2),
+ blurRadius: 4,
+ color: Colors.black54,
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ },
+ );
+ }
+
Widget _buildCategoryHeader(int categoryId, String categoryName) {
final isExpanded = _expandedCategoryId == categoryId;
@@ -408,33 +487,8 @@ class _MenuBrowseScreenState extends State {
child: Stack(
fit: StackFit.expand,
children: [
- // Category image background
- Image.network(
- "$_imageBaseUrl/categories/$categoryId.png",
- fit: BoxFit.cover,
- semanticLabel: categoryName,
- errorBuilder: (context, error, stackTrace) {
- return Image.network(
- "$_imageBaseUrl/categories/$categoryId.jpg",
- fit: BoxFit.cover,
- semanticLabel: categoryName,
- errorBuilder: (context, error, stackTrace) {
- // No image - show category name as fallback
- return Container(
- color: Theme.of(context).colorScheme.primaryContainer,
- alignment: Alignment.center,
- child: Text(
- categoryName,
- style: Theme.of(context).textTheme.headlineSmall?.copyWith(
- fontWeight: FontWeight.bold,
- color: Theme.of(context).colorScheme.onPrimaryContainer,
- ),
- ),
- );
- },
- );
- },
- ),
+ // Category image background or styled text fallback
+ _buildCategoryBackground(categoryId, categoryName),
// Top edge gradient
Positioned(
top: 0,
@@ -473,27 +527,6 @@ class _MenuBrowseScreenState extends State {
),
),
),
- // Expand/collapse indicator
- Positioned(
- right: 16,
- bottom: 12,
- child: AnimatedRotation(
- turns: isExpanded ? 0.5 : 0,
- duration: const Duration(milliseconds: 300),
- child: Container(
- padding: const EdgeInsets.all(4),
- decoration: BoxDecoration(
- color: Colors.black.withAlpha(100),
- shape: BoxShape.circle,
- ),
- child: const Icon(
- Icons.keyboard_arrow_down,
- color: Colors.white,
- size: 24,
- ),
- ),
- ),
- ),
],
),
),
@@ -836,18 +869,17 @@ class _ItemCustomizationSheetState extends State<_ItemCustomizationSheet> {
_initializeDefaults(widget.item.itemId);
}
- /// Recursively initialize default selections
+ /// Recursively initialize default selections for the ENTIRE tree
+ /// This ensures defaults are pre-selected even for nested items
void _initializeDefaults(int parentId) {
final children = widget.itemsByParent[parentId] ?? [];
- print('[Customization] _initializeDefaults for parentId=$parentId, found ${children.length} children');
for (final child in children) {
- print('[Customization] Child ${child.name} (ID=${child.itemId}): isCheckedByDefault=${child.isCheckedByDefault}');
if (child.isCheckedByDefault) {
_selectedItemIds.add(child.itemId);
- _defaultItemIds.add(child.itemId); // Remember this was a default
- print('[Customization] -> Added to defaults and selected');
- _initializeDefaults(child.itemId);
+ _defaultItemIds.add(child.itemId);
}
+ // Always recurse into all children to find nested defaults
+ _initializeDefaults(child.itemId);
}
}
@@ -891,10 +923,22 @@ class _ItemCustomizationSheetState extends State<_ItemCustomizationSheet> {
return false;
}
- // Recursively validate selected children
- for (final child in selectedChildren) {
- if (!validateRecursive(child.itemId, child)) {
- return false;
+ // Recursively validate ALL children that are modifier groups (have their own children)
+ // This ensures we check required selections in nested modifier groups
+ for (final child in children) {
+ final hasGrandchildren = widget.itemsByParent.containsKey(child.itemId) &&
+ (widget.itemsByParent[child.itemId]?.isNotEmpty ?? false);
+
+ if (hasGrandchildren) {
+ // This is a modifier group - always validate it
+ if (!validateRecursive(child.itemId, child)) {
+ return false;
+ }
+ } else if (_selectedItemIds.contains(child.itemId)) {
+ // This is a leaf option that's selected - validate its children (if any)
+ if (!validateRecursive(child.itemId, child)) {
+ return false;
+ }
}
}
diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart
index 308c317..4b47ccd 100644
--- a/lib/screens/splash_screen.dart
+++ b/lib/screens/splash_screen.dart
@@ -1,11 +1,14 @@
import "dart:async";
+import "dart:math";
import "package:flutter/material.dart";
import "package:provider/provider.dart";
+import "package:dchs_flutter_beacon/dchs_flutter_beacon.dart";
import "../app/app_router.dart";
import "../app/app_state.dart";
import "../services/api.dart";
import "../services/auth_storage.dart";
+import "../services/beacon_permissions.dart";
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@@ -14,60 +17,397 @@ class SplashScreen extends StatefulWidget {
State createState() => _SplashScreenState();
}
-class _SplashScreenState extends State {
- Timer? _timer;
+class _SplashScreenState extends State with TickerProviderStateMixin {
+ // Bouncing logo animation
+ late AnimationController _bounceController;
+ double _x = 100;
+ double _y = 100;
+ double _dx = 2.5;
+ double _dy = 2.0;
+ Color _logoColor = Colors.white;
+ final Random _random = Random();
+
+ // Rotating status text
+ Timer? _statusTimer;
+ int _statusIndex = 0;
+ static const List _statusPhrases = [
+ "scanning...",
+ "listening...",
+ "searching...",
+ "locating...",
+ "analyzing...",
+ "connecting...",
+ ];
+
+ // Beacon scanning state
+ Map _uuidToBeaconId = {};
+ final Map> _beaconRssiSamples = {};
+ final Map _beaconDetectionCount = {};
+ bool _scanComplete = false;
+ BeaconResult? _bestBeacon;
+
+ static const List _colors = [
+ Colors.white,
+ Colors.red,
+ Colors.green,
+ Colors.blue,
+ Colors.yellow,
+ Colors.purple,
+ Colors.cyan,
+ Colors.orange,
+ ];
@override
void initState() {
super.initState();
- print('[Splash] đ SplashScreen initState called');
+ print('[Splash] đ Starting with bouncing logo + beacon scan');
- _timer = Timer(const Duration(milliseconds: 2400), () async {
- print('[Splash] â° Timer fired, starting navigation logic');
- if (!mounted) return;
+ // Start bouncing animation
+ _bounceController = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 16), // ~60fps
+ )..addListener(_updatePosition)..repeat();
- // Check for saved authentication credentials
- print('[Splash] đ Checking for saved auth credentials...');
- final credentials = await AuthStorage.loadAuth();
- if (credentials != null) {
- print('[Splash] â
Found saved credentials: UserID=${credentials.userId}');
- // Restore authentication state
- Api.setAuthToken(credentials.token);
- final appState = context.read();
- appState.setUserId(credentials.userId);
- } else {
- print('[Splash] âšī¸ No saved credentials found');
+ // Start rotating status text (randomized)
+ _statusTimer = Timer.periodic(const Duration(milliseconds: 1600), (_) {
+ if (mounted) {
+ setState(() {
+ int newIndex;
+ do {
+ newIndex = _random.nextInt(_statusPhrases.length);
+ } while (newIndex == _statusIndex && _statusPhrases.length > 1);
+ _statusIndex = newIndex;
+ });
+ }
+ });
+
+ // Start the initialization flow
+ _initializeApp();
+ }
+
+ void _updatePosition() {
+ if (!mounted) return;
+
+ final size = MediaQuery.of(context).size;
+ const logoWidth = 180.0;
+ const logoHeight = 60.0;
+
+ setState(() {
+ _x += _dx;
+ _y += _dy;
+
+ // Bounce off edges and change color
+ if (_x <= 0 || _x >= size.width - logoWidth) {
+ _dx = -_dx;
+ _changeColor();
+ }
+ if (_y <= 0 || _y >= size.height - logoHeight) {
+ _dy = -_dy;
+ _changeColor();
}
- if (!mounted) return;
-
- // Always go to beacon scan first - allows browsing without login
- print('[Splash] đĄ Navigating to beacon scan screen');
- Navigator.of(context).pushReplacementNamed(AppRoutes.beaconScan);
+ // Keep in bounds
+ _x = _x.clamp(0, size.width - logoWidth);
+ _y = _y.clamp(0, size.height - logoHeight);
});
}
+ void _changeColor() {
+ final newColor = _colors[_random.nextInt(_colors.length)];
+ if (newColor != _logoColor) {
+ _logoColor = newColor;
+ }
+ }
+
+ Future _initializeApp() async {
+ // Check for saved auth credentials
+ print('[Splash] đ Checking for saved auth credentials...');
+ final credentials = await AuthStorage.loadAuth();
+ if (credentials != null && mounted) {
+ print('[Splash] â
Found saved credentials: UserID=${credentials.userId}');
+ Api.setAuthToken(credentials.token);
+ final appState = context.read();
+ appState.setUserId(credentials.userId);
+ }
+
+ // Start beacon scanning in background
+ await _performBeaconScan();
+
+ // Navigate based on results
+ if (!mounted) return;
+ _navigateToNextScreen();
+ }
+
+ Future _performBeaconScan() async {
+ print('[Splash] đĄ Starting beacon scan...');
+
+ // Request permissions
+ final granted = await BeaconPermissions.requestPermissions();
+ if (!granted) {
+ print('[Splash] â Permissions denied');
+ _scanComplete = true;
+ return;
+ }
+
+ // Fetch beacon list from server
+ try {
+ _uuidToBeaconId = await Api.listAllBeacons();
+ print('[Splash] Loaded ${_uuidToBeaconId.length} beacons from database');
+ } catch (e) {
+ print('[Splash] Error loading beacons: $e');
+ _scanComplete = true;
+ return;
+ }
+
+ if (_uuidToBeaconId.isEmpty) {
+ print('[Splash] No beacons in database');
+ _scanComplete = true;
+ return;
+ }
+
+ // Initialize beacon scanning
+ try {
+ await flutterBeacon.initializeScanning;
+ await Future.delayed(const Duration(milliseconds: 500));
+
+ // Create regions for all known UUIDs
+ final regions = _uuidToBeaconId.keys.map((uuid) {
+ final formattedUUID = '${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}';
+ return Region(identifier: uuid, proximityUUID: formattedUUID);
+ }).toList();
+
+ // Perform scan cycles
+ for (int scanCycle = 1; scanCycle <= 5; scanCycle++) {
+ print('[Splash] ----- Scan cycle $scanCycle/5 -----');
+
+ StreamSubscription? subscription;
+ subscription = flutterBeacon.ranging(regions).listen((result) {
+ for (var beacon in result.beacons) {
+ final uuid = beacon.proximityUUID.toUpperCase().replaceAll('-', '');
+ final rssi = beacon.rssi;
+
+ if (_uuidToBeaconId.containsKey(uuid)) {
+ _beaconRssiSamples.putIfAbsent(uuid, () => []).add(rssi);
+ _beaconDetectionCount[uuid] = (_beaconDetectionCount[uuid] ?? 0) + 1;
+ print('[Splash] â
Beacon ${_uuidToBeaconId[uuid]}, RSSI=$rssi');
+ }
+ }
+ });
+
+ await Future.delayed(const Duration(seconds: 2));
+ await subscription.cancel();
+
+ // Check for early exit after 3 cycles
+ if (scanCycle >= 3 && _beaconRssiSamples.isNotEmpty && _canExitEarly()) {
+ print('[Splash] ⥠Early exit - stable readings');
+ break;
+ }
+
+ if (scanCycle < 5) {
+ await Future.delayed(const Duration(milliseconds: 200));
+ }
+ }
+
+ // Find best beacon
+ _bestBeacon = _findBestBeacon();
+ print('[Splash] đ¯ Best beacon: ${_bestBeacon?.beaconId ?? "none"}');
+
+ } catch (e) {
+ print('[Splash] Scan error: $e');
+ }
+
+ _scanComplete = true;
+ }
+
+ bool _canExitEarly() {
+ if (_beaconRssiSamples.isEmpty) return false;
+
+ bool hasEnoughSamples = _beaconRssiSamples.values.any((s) => s.length >= 3);
+ if (!hasEnoughSamples) return false;
+
+ for (final entry in _beaconRssiSamples.entries) {
+ final samples = entry.value;
+ if (samples.length < 3) continue;
+
+ final avg = samples.reduce((a, b) => a + b) / samples.length;
+ final variance = samples.map((r) => (r - avg) * (r - avg)).reduce((a, b) => a + b) / samples.length;
+
+ if (variance > 50) return false;
+ }
+
+ if (_beaconRssiSamples.length > 1) {
+ final avgRssis = {};
+ for (final entry in _beaconRssiSamples.entries) {
+ if (entry.value.isNotEmpty) {
+ avgRssis[entry.key] = entry.value.reduce((a, b) => a + b) / entry.value.length;
+ }
+ }
+ final sorted = avgRssis.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
+ if (sorted.length >= 2 && (sorted[0].value - sorted[1].value) < 5) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ BeaconResult? _findBestBeacon() {
+ if (_beaconRssiSamples.isEmpty) return null;
+
+ String? bestUuid;
+ double bestAvgRssi = -999;
+
+ for (final entry in _beaconRssiSamples.entries) {
+ final samples = entry.value;
+ final detections = _beaconDetectionCount[entry.key] ?? 0;
+
+ if (detections < 3) continue;
+
+ final avgRssi = samples.reduce((a, b) => a + b) / samples.length;
+ if (avgRssi > bestAvgRssi && avgRssi >= -85) {
+ bestAvgRssi = avgRssi;
+ bestUuid = entry.key;
+ }
+ }
+
+ if (bestUuid != null) {
+ return BeaconResult(
+ uuid: bestUuid,
+ beaconId: _uuidToBeaconId[bestUuid]!,
+ avgRssi: bestAvgRssi,
+ );
+ }
+
+ // Fall back to strongest signal even if doesn't meet threshold
+ if (_beaconRssiSamples.isNotEmpty) {
+ for (final entry in _beaconRssiSamples.entries) {
+ final samples = entry.value;
+ if (samples.isEmpty) continue;
+ final avgRssi = samples.reduce((a, b) => a + b) / samples.length;
+ if (avgRssi > bestAvgRssi) {
+ bestAvgRssi = avgRssi;
+ bestUuid = entry.key;
+ }
+ }
+ if (bestUuid != null) {
+ return BeaconResult(
+ uuid: bestUuid,
+ beaconId: _uuidToBeaconId[bestUuid]!,
+ avgRssi: bestAvgRssi,
+ );
+ }
+ }
+
+ return null;
+ }
+
+ Future _navigateToNextScreen() async {
+ if (!mounted) return;
+
+ if (_bestBeacon != null) {
+ // Auto-select business from beacon
+ try {
+ final mapping = await Api.getBusinessFromBeacon(beaconId: _bestBeacon!.beaconId);
+
+ if (!mounted) return;
+
+ final appState = context.read();
+ appState.setBusinessAndServicePoint(
+ mapping.businessId,
+ mapping.servicePointId,
+ businessName: mapping.businessName,
+ servicePointName: mapping.servicePointName,
+ );
+ Api.setBusinessId(mapping.businessId);
+
+ print('[Splash] đ Auto-selected: ${mapping.businessName}');
+
+ Navigator.of(context).pushReplacementNamed(
+ AppRoutes.menuBrowse,
+ arguments: {
+ 'businessId': mapping.businessId,
+ 'servicePointId': mapping.servicePointId,
+ },
+ );
+ return;
+ } catch (e) {
+ print('[Splash] Error mapping beacon to business: $e');
+ }
+ }
+
+ // No beacon or error - go to restaurant select
+ print('[Splash] Going to restaurant select');
+ Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect);
+ }
+
@override
void dispose() {
- _timer?.cancel();
+ _bounceController.dispose();
+ _statusTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
- return const Scaffold(
+ return Scaffold(
backgroundColor: Colors.black,
- body: Center(
- child: Text(
- "PAYFRIT",
- style: TextStyle(
- color: Colors.white,
- fontSize: 38,
- fontWeight: FontWeight.w800,
- letterSpacing: 3,
+ body: Stack(
+ children: [
+ // Centered static status text
+ Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Text(
+ "site survey",
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 18,
+ fontWeight: FontWeight.w300,
+ letterSpacing: 1,
+ ),
+ ),
+ const SizedBox(height: 6),
+ Text(
+ _statusPhrases[_statusIndex],
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 18,
+ fontWeight: FontWeight.w300,
+ letterSpacing: 1,
+ ),
+ ),
+ ],
+ ),
),
- ),
+ // Bouncing logo
+ Positioned(
+ left: _x,
+ top: _y,
+ child: Text(
+ "PAYFRIT",
+ style: TextStyle(
+ color: _logoColor,
+ fontSize: 38,
+ fontWeight: FontWeight.w800,
+ letterSpacing: 3,
+ ),
+ ),
+ ),
+ ],
),
);
}
}
+
+class BeaconResult {
+ final String uuid;
+ final int beaconId;
+ final double avgRssi;
+
+ const BeaconResult({
+ required this.uuid,
+ required this.beaconId,
+ required this.avgRssi,
+ });
+}
diff --git a/pubspec.lock b/pubspec.lock
index 194a722..ee6eacc 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1,6 +1,22 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ archive:
+ dependency: transitive
+ description:
+ name: archive
+ sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.7"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.7.0"
async:
dependency: transitive
description:
@@ -25,6 +41,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.4"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.2"
clock:
dependency: transitive
description:
@@ -41,6 +73,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.7"
dchs_flutter_beacon:
dependency: "direct main"
description:
@@ -78,6 +118,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_launcher_icons:
+ dependency: "direct dev"
+ description:
+ name: flutter_launcher_icons
+ sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.13.1"
flutter_lints:
dependency: "direct dev"
description:
@@ -112,6 +160,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
+ image:
+ dependency: transitive
+ description:
+ name: image
+ sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.7.2"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.9.0"
leak_tracker:
dependency: transitive
description:
@@ -256,6 +320,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.1"
platform:
dependency: transitive
description:
@@ -272,6 +344,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
+ posix:
+ dependency: transitive
+ description:
+ name: posix
+ sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.3"
provider:
dependency: "direct main"
description:
@@ -429,6 +509,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.6.1"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.3"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 717d754..bc91b28 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -20,6 +20,15 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
+ flutter_launcher_icons: ^0.13.1
+
+flutter_launcher_icons:
+ android: true
+ ios: true
+ image_path: "icon.png"
+ adaptive_icon_background: "#000000"
+ adaptive_icon_foreground: "icon.png"
+ remove_alpha_ios: true
flutter:
uses-material-design: true