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