Compare commits
17 Commits
main
...
version_tw
| Author | SHA1 | Date |
|---|---|---|
|
|
1fed631f1b | |
|
|
2a7d83cf41 | |
|
|
28cf436dad | |
|
|
0a80b8d817 | |
|
|
44b0f47648 | |
|
|
c630dda016 | |
|
|
4b35077fba | |
|
|
8096cad270 | |
|
|
bf87873181 | |
|
|
a3db70ffc6 | |
|
|
3f2a804a31 | |
|
|
ac1294a150 | |
|
|
45a3e381c0 | |
|
|
67142977c0 | |
|
|
4acfdb9c4a | |
|
|
ca87000e87 | |
|
|
6f11b367b6 |
|
|
@ -1,8 +1,15 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="pozo_app"
|
android:label="PozoApp"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/launcher_icon">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="?android:colorBackground" />
|
<item android:drawable="@color/splash_bg" />
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
<!-- You can insert your own image assets here -->
|
||||||
<!-- <item>
|
<!-- <item>
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@android:color/white" />
|
<item android:drawable="@color/splash_bg" />
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
<!-- You can insert your own image assets here -->
|
||||||
<!-- <item>
|
<!-- <item>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 611 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -5,6 +5,7 @@
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">@color/splash_bg</item>
|
||||||
</style>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="splash_bg">#d2242e</color>
|
||||||
|
<color name="ic_launcher_background">#d2242e</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">@color/splash_bg</item>
|
||||||
</style>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,28 @@ rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
subprojects {
|
subprojects {
|
||||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
if (extensions.findByName("android") != null) {
|
||||||
|
val android = extensions.findByName("android") as com.android.build.gradle.BaseExtension
|
||||||
|
if (android.namespace == null) {
|
||||||
|
var manifestPackage: String? = null
|
||||||
|
val manifestFile = project.projectDir.resolve("src/main/AndroidManifest.xml")
|
||||||
|
if (manifestFile.exists()) {
|
||||||
|
val content = manifestFile.readText()
|
||||||
|
val match = Regex("package=\"([^\"]+)\"").find(content)
|
||||||
|
if (match != null) {
|
||||||
|
manifestPackage = match.groupValues[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (manifestPackage != null) {
|
||||||
|
android.namespace = manifestPackage
|
||||||
|
} else {
|
||||||
|
android.namespace = "co.gittouch." + project.name.replace("-", "_")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
subprojects {
|
subprojects {
|
||||||
project.evaluationDependsOn(":app")
|
project.evaluationDependsOn(":app")
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
|
@ -427,7 +427,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
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_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|
@ -484,7 +484,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
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_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 208 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 437 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 708 B |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 301 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 665 B |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 437 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 559 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 710 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 965 B |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.6 KiB |
|
|
@ -5,7 +5,7 @@
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Pozo App</string>
|
<string>PozoApp</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>pozo_app</string>
|
<string>PozoApp</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|
@ -45,5 +45,19 @@
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>PozoApp requires camera access to capture shop facade photos and document images for business verification.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>PozoApp requires photo gallery access to upload documents and shop facade images.</string>
|
||||||
|
<key>NSContactsUsageDescription</key>
|
||||||
|
<string>PozoApp requires contacts access to let you search and auto-fill mobile numbers during registration and billing.</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>PozoApp needs your location to automatically fetch and verify your shop's address.</string>
|
||||||
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
|
<string>PozoApp needs your location to automatically fetch and verify your shop's address.</string>
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>tel</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
|
import 'package:vibration/vibration.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb, debugPrint;
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
class AppFeedback {
|
||||||
|
// Global settings for sound and haptic vibration feedback
|
||||||
|
static bool isSoundEnabled = true;
|
||||||
|
static bool isHapticEnabled = true;
|
||||||
|
|
||||||
|
static final Map<String, AudioPlayer> _players = {
|
||||||
|
'click.mp3': AudioPlayer(),
|
||||||
|
'select.mp3': AudioPlayer(),
|
||||||
|
'success.mp3': AudioPlayer(),
|
||||||
|
'error.mp3': AudioPlayer(),
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool _initialized = false;
|
||||||
|
|
||||||
|
/// Preload sounds to achieve zero latency
|
||||||
|
static void init() {
|
||||||
|
if (_initialized) return;
|
||||||
|
try {
|
||||||
|
_players.forEach((key, player) {
|
||||||
|
player.setReleaseMode(ReleaseMode.stop);
|
||||||
|
player.setSource(AssetSource('sounds/$key'));
|
||||||
|
});
|
||||||
|
_initialized = true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error initializing AppFeedback: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Play a sound from assets/sounds/ with zero delay
|
||||||
|
static Future<void> playSound(String fileName) async {
|
||||||
|
if (!isSoundEnabled) return;
|
||||||
|
try {
|
||||||
|
if (!_initialized) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
final player = _players[fileName];
|
||||||
|
if (player != null) {
|
||||||
|
// Stop/seek and resume for instant audio
|
||||||
|
await player.seek(Duration.zero);
|
||||||
|
await player.resume();
|
||||||
|
} else {
|
||||||
|
// Fallback for dynamically added files
|
||||||
|
final tempPlayer = AudioPlayer();
|
||||||
|
await tempPlayer.play(AssetSource('sounds/$fileName'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Audio playback error for $fileName: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to check Android platform safely
|
||||||
|
static bool get _isAndroid => !kIsWeb && Platform.isAndroid;
|
||||||
|
|
||||||
|
/// Trigger native vibration for Android with customized duration (in milliseconds)
|
||||||
|
static void _triggerVibration(int duration) async {
|
||||||
|
if (!isHapticEnabled || kIsWeb) return;
|
||||||
|
try {
|
||||||
|
if (_isAndroid) {
|
||||||
|
if (await Vibration.hasVibrator() == true) {
|
||||||
|
Vibration.vibrate(duration: duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Vibration error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Light haptic feedback + subtle click sound
|
||||||
|
static void click() {
|
||||||
|
if (isHapticEnabled) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
_triggerVibration(30);
|
||||||
|
}
|
||||||
|
playSound('click.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Medium haptic + selection sound
|
||||||
|
static void select() {
|
||||||
|
if (isHapticEnabled) {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
_triggerVibration(60);
|
||||||
|
}
|
||||||
|
playSound('select.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Celebration sound + strong haptic vibration
|
||||||
|
static void success() {
|
||||||
|
if (isHapticEnabled) {
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
_doubleVibrate();
|
||||||
|
}
|
||||||
|
playSound('success.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrong input -> error haptic (double vibrate) + error sound
|
||||||
|
static void error() {
|
||||||
|
if (isHapticEnabled) {
|
||||||
|
_doubleVibrate();
|
||||||
|
}
|
||||||
|
playSound('error.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Light haptic feedback (e.g. chip selections)
|
||||||
|
static void lightHaptic() {
|
||||||
|
if (!isHapticEnabled) return;
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
_triggerVibration(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Medium haptic feedback
|
||||||
|
static void mediumHaptic() {
|
||||||
|
if (!isHapticEnabled) return;
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
_triggerVibration(60);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Heavy haptic feedback
|
||||||
|
static void heavyHaptic() {
|
||||||
|
if (!isHapticEnabled) return;
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
_triggerVibration(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subtle haptic feedback (e.g. screen navigation)
|
||||||
|
static void subtleHaptic() {
|
||||||
|
if (!isHapticEnabled) return;
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
_triggerVibration(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Double vibrate pattern using vibration package
|
||||||
|
static Future<void> _doubleVibrate() async {
|
||||||
|
if (!isHapticEnabled) return;
|
||||||
|
try {
|
||||||
|
if (!kIsWeb && await Vibration.hasVibrator() == true) {
|
||||||
|
// Double vibrate pattern: wait 0, vibrate 100ms, wait 100ms, vibrate 100ms
|
||||||
|
Vibration.vibrate(pattern: [0, 100, 100, 100]);
|
||||||
|
} else {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
import 'screens/welcome_screen.dart';
|
import 'screens/welcome_screen.dart';
|
||||||
|
import 'feedback_helper.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
AppFeedback.init();
|
||||||
runApp(const PozoApp());
|
runApp(const PozoApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'feedback_helper.dart';
|
||||||
|
|
||||||
|
class FadeSlidePageRoute<T> extends PageRouteBuilder<T> {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
FadeSlidePageRoute({required this.child})
|
||||||
|
: super(
|
||||||
|
pageBuilder: (context, animation, secondaryAnimation) => child,
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
|
final fadeAnimation = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
|
||||||
|
final slideAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(0.0, 0.05),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: fadeAnimation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: slideAnimation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
transitionDuration: const Duration(milliseconds: 300),
|
||||||
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
||||||
|
) {
|
||||||
|
AppFeedback.subtleHaptic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showTopAlert(
|
||||||
|
BuildContext context,
|
||||||
|
String message, {
|
||||||
|
bool isError = false,
|
||||||
|
IconData? icon,
|
||||||
|
}) {
|
||||||
|
final overlayState = Overlay.of(context);
|
||||||
|
late OverlayEntry overlayEntry;
|
||||||
|
|
||||||
|
overlayEntry = OverlayEntry(
|
||||||
|
builder: (context) => _TopAlertWidget(
|
||||||
|
message: message,
|
||||||
|
isError: isError,
|
||||||
|
icon: icon,
|
||||||
|
onDismiss: () {
|
||||||
|
overlayEntry.remove();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
overlayState.insert(overlayEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TopAlertWidget extends StatefulWidget {
|
||||||
|
final String message;
|
||||||
|
final bool isError;
|
||||||
|
final IconData? icon;
|
||||||
|
final VoidCallback onDismiss;
|
||||||
|
|
||||||
|
const _TopAlertWidget({
|
||||||
|
required this.message,
|
||||||
|
required this.isError,
|
||||||
|
this.icon,
|
||||||
|
required this.onDismiss,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TopAlertWidget> createState() => _TopAlertWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TopAlertWidgetState extends State<_TopAlertWidget> with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _slideAnimation;
|
||||||
|
late Animation<double> _opacityAnimation;
|
||||||
|
Timer? _dismissTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimation = CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeOutBack,
|
||||||
|
);
|
||||||
|
|
||||||
|
_opacityAnimation = CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
);
|
||||||
|
|
||||||
|
_controller.forward();
|
||||||
|
|
||||||
|
_dismissTimer = Timer(const Duration(milliseconds: 2800), () {
|
||||||
|
if (mounted) {
|
||||||
|
_controller.reverse().then((_) {
|
||||||
|
widget.onDismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_dismissTimer?.cancel();
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final topPadding = mediaQuery.padding.top + 16.0;
|
||||||
|
|
||||||
|
IconData alertIcon = widget.icon ??
|
||||||
|
(widget.isError ? Icons.error_outline : Icons.check_circle_outline);
|
||||||
|
Color backgroundColor = widget.isError ? const Color(0xFFBA1A20) : const Color(0xFF2E7D32);
|
||||||
|
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Positioned(
|
||||||
|
top: topPadding * _slideAnimation.value - 80.0 * (1.0 - _slideAnimation.value),
|
||||||
|
left: 16.0,
|
||||||
|
right: 16.0,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _opacityAnimation.value,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 450),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.12),
|
||||||
|
blurRadius: 16,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
alertIcon,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.message,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SlideFadeEntrance extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final Duration delay;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
const SlideFadeEntrance({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.delay = Duration.zero,
|
||||||
|
this.duration = const Duration(milliseconds: 500),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SlideFadeEntrance> createState() => _SlideFadeEntranceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SlideFadeEntranceState extends State<SlideFadeEntrance> with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _opacityAnimation;
|
||||||
|
late Animation<double> _slideAnimation;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: widget.duration,
|
||||||
|
);
|
||||||
|
|
||||||
|
_opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeIn),
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimation = Tween<double>(begin: 12.0, end: 0.0).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (widget.delay == Duration.zero) {
|
||||||
|
_controller.forward();
|
||||||
|
} else {
|
||||||
|
_timer = Timer(widget.delay, () {
|
||||||
|
if (mounted) {
|
||||||
|
_controller.forward();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: _opacityAnimation.value,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset(0.0, _slideAnimation.value),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InteractiveScale extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final double hoverScale;
|
||||||
|
final double tapScale;
|
||||||
|
|
||||||
|
const InteractiveScale({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.onTap,
|
||||||
|
this.hoverScale = 1.03,
|
||||||
|
this.tapScale = 0.96,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InteractiveScale> createState() => _InteractiveScaleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InteractiveScaleState extends State<InteractiveScale> {
|
||||||
|
double _scale = 1.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => setState(() => _scale = widget.hoverScale),
|
||||||
|
onExit: (_) => setState(() => _scale = 1.0),
|
||||||
|
child: AnimatedScale(
|
||||||
|
scale: _scale,
|
||||||
|
duration: const Duration(milliseconds: 120),
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTapDown: (_) => setState(() => _scale = widget.tapScale),
|
||||||
|
onTapUp: (_) => setState(() => _scale = widget.hoverScale),
|
||||||
|
onTapCancel: () => setState(() => _scale = 1.0),
|
||||||
|
onTap: widget.onTap,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../route_transitions.dart';
|
||||||
|
import '../feedback_helper.dart';
|
||||||
|
|
||||||
class ProductItem {
|
class ProductItem {
|
||||||
final String id;
|
final String id;
|
||||||
|
|
@ -135,6 +137,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _simulateBarcodeScan() {
|
void _simulateBarcodeScan() {
|
||||||
|
AppFeedback.click();
|
||||||
if (!widget.barcodeScanner) return;
|
if (!widget.barcodeScanner) return;
|
||||||
final random = Random();
|
final random = Random();
|
||||||
final randomProduct = _products[random.nextInt(_products.length)];
|
final randomProduct = _products[random.nextInt(_products.length)];
|
||||||
|
|
@ -160,13 +163,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showErrorSnackBar(String message) {
|
void _showErrorSnackBar(String message) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(context, message, isError: true);
|
||||||
SnackBar(
|
|
||||||
content: Text(message),
|
|
||||||
backgroundColor: Colors.red[800],
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double get _subtotal {
|
double get _subtotal {
|
||||||
|
|
@ -182,6 +179,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
double get _total => _subtotal + _cgst + _sgst;
|
double get _total => _subtotal + _cgst + _sgst;
|
||||||
|
|
||||||
void _handleReviewAndGenerate() {
|
void _handleReviewAndGenerate() {
|
||||||
|
AppFeedback.success();
|
||||||
final random = Random();
|
final random = Random();
|
||||||
final invoiceVal = 'POZO-${random.nextInt(90000) + 10000}';
|
final invoiceVal = 'POZO-${random.nextInt(90000) + 10000}';
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -191,6 +189,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _closeReceiptAndReset() {
|
void _closeReceiptAndReset() {
|
||||||
|
AppFeedback.click();
|
||||||
setState(() {
|
setState(() {
|
||||||
_printingInvoice = false;
|
_printingInvoice = false;
|
||||||
_billingList.clear();
|
_billingList.clear();
|
||||||
|
|
@ -211,7 +210,10 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -253,6 +255,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: Directionality(
|
child: Directionality(
|
||||||
textDirection: TextDirection.rtl,
|
textDirection: TextDirection.rtl,
|
||||||
|
child: InteractiveScale(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: _simulateBarcodeScan,
|
onPressed: _simulateBarcodeScan,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
@ -272,6 +275,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (widget.kotManagement)
|
if (widget.kotManagement)
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
|
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
|
||||||
|
|
@ -412,7 +416,13 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
final isLowStock = widget.inventoryAlerts && remainingStock <= 3;
|
final isLowStock = widget.inventoryAlerts && remainingStock <= 3;
|
||||||
final isOutOfStock = widget.inventoryAlerts && remainingStock <= 0;
|
final isOutOfStock = widget.inventoryAlerts && remainingStock <= 0;
|
||||||
|
|
||||||
return InkWell(
|
return SlideFadeEntrance(
|
||||||
|
delay: Duration(milliseconds: index * 40),
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: InteractiveScale(
|
||||||
|
tapScale: isOutOfStock ? 1.0 : 0.95,
|
||||||
|
hoverScale: isOutOfStock ? 1.0 : 1.03,
|
||||||
|
child: InkWell(
|
||||||
onTap: isOutOfStock ? null : () => _handleAddItem(prod),
|
onTap: isOutOfStock ? null : () => _handleAddItem(prod),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
@ -503,6 +513,8 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -660,12 +672,18 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
const Divider(color: Color(0xFF334155), height: 16),
|
const Divider(color: Color(0xFF334155), height: 16),
|
||||||
_buildComputationCalculations(theme),
|
_buildComputationCalculations(theme),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
ElevatedButton(
|
InteractiveScale(
|
||||||
|
hoverScale: _billingList.isNotEmpty ? 1.03 : 1.0,
|
||||||
|
tapScale: _billingList.isNotEmpty ? 0.96 : 1.0,
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: _billingList.isEmpty ? null : _handleReviewAndGenerate,
|
onPressed: _billingList.isEmpty ? null : _handleReviewAndGenerate,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: theme.colorScheme.primary,
|
backgroundColor: theme.colorScheme.primary,
|
||||||
disabledBackgroundColor: const Color(0xFF1E293B),
|
disabledBackgroundColor: const Color(0xFF1E293B),
|
||||||
minimumSize: const Size.fromHeight(48),
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: const Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|
@ -675,6 +693,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
Text('Generate Invoice'),
|
Text('Generate Invoice'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
@ -756,11 +775,17 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
InteractiveScale(
|
||||||
|
hoverScale: _billingList.isNotEmpty ? 1.03 : 1.0,
|
||||||
|
tapScale: _billingList.isNotEmpty ? 0.96 : 1.0,
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: _billingList.isEmpty ? null : _handleReviewAndGenerate,
|
onPressed: _billingList.isEmpty ? null : _handleReviewAndGenerate,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
minimumSize: const Size(180, 44),
|
minimumSize: const Size(180, 44),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: const Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
@ -770,6 +795,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
Text('Review & Invoice', style: TextStyle(fontSize: 12)),
|
Text('Review & Invoice', style: TextStyle(fontSize: 12)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -942,14 +968,14 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
InteractiveScale(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppFeedback.click();
|
||||||
SnackBar(
|
showTopAlert(
|
||||||
content: Text('🧾 Thermal Printed: $_invoiceNumber successfully!'),
|
context,
|
||||||
backgroundColor: const Color(0xFF10B981),
|
'Thermal Printed: $_invoiceNumber successfully!',
|
||||||
behavior: SnackBarBehavior.floating,
|
icon: Icons.print,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.print, size: 16),
|
icon: const Icon(Icons.print, size: 16),
|
||||||
|
|
@ -958,17 +984,26 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
minimumSize: const Size.fromHeight(42),
|
minimumSize: const Size.fromHeight(42),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ElevatedButton(
|
InteractiveScale(
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: _closeReceiptAndReset,
|
onPressed: _closeReceiptAndReset,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: theme.colorScheme.primary,
|
backgroundColor: theme.colorScheme.primary,
|
||||||
minimumSize: const Size.fromHeight(42),
|
minimumSize: const Size.fromHeight(42),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Text('New Ledger Sale', style: TextStyle(fontSize: 12)),
|
child: const Text('New Ledger Sale', style: TextStyle(fontSize: 12)),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:geocoding/geocoding.dart';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import 'dart:io';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
|
import '../route_transitions.dart';
|
||||||
|
import '../feedback_helper.dart';
|
||||||
import 'customize_setup_screen.dart';
|
import 'customize_setup_screen.dart';
|
||||||
|
|
||||||
class BusinessSetupScreen extends StatefulWidget {
|
class BusinessSetupScreen extends StatefulWidget {
|
||||||
|
|
@ -13,10 +21,20 @@ class BusinessSetupScreen extends StatefulWidget {
|
||||||
class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
final TextEditingController _nameController = TextEditingController();
|
final TextEditingController _nameController = TextEditingController();
|
||||||
final TextEditingController _gstinController = TextEditingController();
|
final TextEditingController _gstinController = TextEditingController();
|
||||||
|
final TextEditingController _addressController = TextEditingController();
|
||||||
|
final TextEditingController _fssaiController = TextEditingController();
|
||||||
|
final TextEditingController _udhyogController = TextEditingController();
|
||||||
|
final TextEditingController _panController = TextEditingController();
|
||||||
|
final TextEditingController _aadharController = TextEditingController();
|
||||||
|
|
||||||
String _selectedCategory = 'supermarket';
|
String _selectedCategory = 'supermarket';
|
||||||
bool _isNameValid = false;
|
bool _isNameValid = false;
|
||||||
|
|
||||||
|
bool _isGstinValid = false;
|
||||||
|
bool _hasGstinInput = false;
|
||||||
|
|
||||||
final Set<String> _selectedDocs = {'GSTIN', 'FSSAI'};
|
final Set<String> _selectedDocs = {'GSTIN', 'FSSAI'};
|
||||||
final Map<String, String> _uploadedDocs = {};
|
final Map<String, XFile> _uploadedDocs = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -26,15 +44,54 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
_isNameValid = _nameController.text.trim().isNotEmpty;
|
_isNameValid = _nameController.text.trim().isNotEmpty;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
_gstinController.addListener(_validateGstin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _validateGstin() {
|
||||||
|
final text = _gstinController.text.trim();
|
||||||
|
if (text.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_hasGstinInput = false;
|
||||||
|
_isGstinValid = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final RegExp gstinRegex = RegExp(r'^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[0-9]{1}Z[A-Z0-9]{1}$');
|
||||||
|
setState(() {
|
||||||
|
_hasGstinInput = true;
|
||||||
|
_isGstinValid = gstinRegex.hasMatch(text);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_nameController.dispose();
|
_nameController.dispose();
|
||||||
_gstinController.dispose();
|
_gstinController.dispose();
|
||||||
|
_addressController.dispose();
|
||||||
|
_fssaiController.dispose();
|
||||||
|
_udhyogController.dispose();
|
||||||
|
_panController.dispose();
|
||||||
|
_aadharController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextEditingController _getControllerForDoc(String docName) {
|
||||||
|
switch (docName) {
|
||||||
|
case 'GSTIN':
|
||||||
|
return _gstinController;
|
||||||
|
case 'FSSAI':
|
||||||
|
return _fssaiController;
|
||||||
|
case 'Udhyog Aadhar':
|
||||||
|
return _udhyogController;
|
||||||
|
case 'PAN':
|
||||||
|
return _panController;
|
||||||
|
case 'Aadhar':
|
||||||
|
return _aadharController;
|
||||||
|
default:
|
||||||
|
return TextEditingController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final List<Map<String, dynamic>> _categories = [
|
final List<Map<String, dynamic>> _categories = [
|
||||||
{'id': 'supermarket', 'name': 'Supermarket', 'icon': Icons.shopping_cart},
|
{'id': 'supermarket', 'name': 'Supermarket', 'icon': Icons.shopping_cart},
|
||||||
{'id': 'restaurant', 'name': 'Restaurant', 'icon': Icons.restaurant},
|
{'id': 'restaurant', 'name': 'Restaurant', 'icon': Icons.restaurant},
|
||||||
|
|
@ -44,18 +101,240 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
{'id': 'others', 'name': 'Others', 'icon': Icons.grid_view},
|
{'id': 'others', 'name': 'Others', 'icon': Icons.grid_view},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Future<void> _handleImagePick(String docName, ImageSource source) async {
|
||||||
|
AppFeedback.click();
|
||||||
|
final permission = source == ImageSource.camera ? Permission.camera : Permission.photos;
|
||||||
|
var status = await permission.status;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (status.isPermanentlyDenied) {
|
||||||
|
_showSettingsDialog(source == ImageSource.camera ? 'Camera' : 'Photos');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.isDenied && !kIsWeb) {
|
||||||
|
final String sourceName = source == ImageSource.camera ? 'Camera' : 'Photos/Gallery';
|
||||||
|
final bool? proceed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('$sourceName Permission Required'),
|
||||||
|
content: Text(
|
||||||
|
'PozoApp requires $sourceName permission to upload document verification files and store facade images.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context, false);
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
},
|
||||||
|
child: const Text('Continue'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
if (proceed != true) return;
|
||||||
|
status = await permission.request();
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (status.isPermanentlyDenied) {
|
||||||
|
_showSettingsDialog(sourceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.isGranted || kIsWeb) {
|
||||||
|
try {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final XFile? file = await picker.pickImage(source: source);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (file != null) {
|
||||||
|
setState(() {
|
||||||
|
_uploadedDocs[docName] = file;
|
||||||
|
});
|
||||||
|
showTopAlert(
|
||||||
|
context,
|
||||||
|
'Successfully loaded $docName!',
|
||||||
|
icon: Icons.check_circle_outline,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
showTopAlert(
|
||||||
|
context,
|
||||||
|
'Failed to pick image: $e',
|
||||||
|
isError: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showTopAlert(
|
||||||
|
context,
|
||||||
|
'Permission denied. Cannot load document image.',
|
||||||
|
isError: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSettingsDialog(String permissionName) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('$permissionName Permission Disabled'),
|
||||||
|
content: Text('Please enable $permissionName access in system settings to proceed.'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
openAppSettings();
|
||||||
|
},
|
||||||
|
child: const Text('Open Settings'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchLocationAndAddress() async {
|
||||||
|
AppFeedback.click();
|
||||||
|
bool serviceEnabled;
|
||||||
|
LocationPermission permission;
|
||||||
|
|
||||||
|
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
|
if (!serviceEnabled) {
|
||||||
|
if (mounted) {
|
||||||
|
showTopAlert(context, 'Location services are disabled.', isError: true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
permission = await Geolocator.checkPermission();
|
||||||
|
if (permission == LocationPermission.denied) {
|
||||||
|
permission = await Geolocator.requestPermission();
|
||||||
|
if (permission == LocationPermission.denied) {
|
||||||
|
if (mounted) {
|
||||||
|
showTopAlert(context, 'Location permissions are denied', isError: true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission == LocationPermission.deniedForever) {
|
||||||
|
if (mounted) {
|
||||||
|
_showSettingsDialog('Location');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
showTopAlert(context, 'Fetching location...', icon: Icons.gps_fixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Position position = await Geolocator.getCurrentPosition(
|
||||||
|
desiredAccuracy: LocationAccuracy.high);
|
||||||
|
|
||||||
|
if (kIsWeb) {
|
||||||
|
// Reverse geocoding might have CORS issues on web without a backend proxy or proper keys depending on provider,
|
||||||
|
// Mocking for web demonstration unless specifically supported
|
||||||
|
setState(() {
|
||||||
|
_addressController.text = 'Web Mock Address: Lat ${position.latitude.toStringAsFixed(4)}, Lng ${position.longitude.toStringAsFixed(4)}\nCity, State, Country';
|
||||||
|
});
|
||||||
|
if (mounted) {
|
||||||
|
showTopAlert(context, 'Location detected (Web Mock)', icon: Icons.my_location_outlined);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List<Placemark> placemarks = await placemarkFromCoordinates(
|
||||||
|
position.latitude, position.longitude);
|
||||||
|
|
||||||
|
if (placemarks.isNotEmpty) {
|
||||||
|
Placemark place = placemarks[0];
|
||||||
|
List<String> parts = [];
|
||||||
|
if (place.name != null && place.name!.isNotEmpty) parts.add(place.name!);
|
||||||
|
if (place.subLocality != null && place.subLocality!.isNotEmpty) parts.add(place.subLocality!);
|
||||||
|
if (place.locality != null && place.locality!.isNotEmpty) parts.add(place.locality!);
|
||||||
|
if (place.administrativeArea != null && place.administrativeArea!.isNotEmpty) parts.add(place.administrativeArea!);
|
||||||
|
if (place.postalCode != null && place.postalCode!.isNotEmpty) parts.add(place.postalCode!);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_addressController.text = parts.join(', ');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
showTopAlert(context, 'Address auto-filled successfully!', icon: Icons.my_location_outlined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
showTopAlert(context, 'Failed to fetch address: $e', isError: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showFacadePhotoOptions() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Wrap(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.camera_alt, color: Color(0xFFAF101A)),
|
||||||
|
title: const Text('Take Photo'),
|
||||||
|
onTap: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
_handleImagePick('facade_photo', ImageSource.camera);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.photo_library, color: Color(0xFFAF101A)),
|
||||||
|
title: const Text('Choose from Gallery'),
|
||||||
|
onTap: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
_handleImagePick('facade_photo', ImageSource.gallery);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDocChip(String label, IconData icon) {
|
Widget _buildDocChip(String label, IconData icon) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final isSelected = _selectedDocs.contains(label);
|
final isSelected = _selectedDocs.contains(label);
|
||||||
return InkWell(
|
return InteractiveScale(
|
||||||
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
AppFeedback.lightHaptic();
|
||||||
if (isSelected) {
|
if (!isSelected) {
|
||||||
_selectedDocs.remove(label);
|
|
||||||
} else {
|
|
||||||
_selectedDocs.add(label);
|
_selectedDocs.add(label);
|
||||||
}
|
_showFacadePhotoOptionsForDoc(label);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_selectedDocs.remove(label);
|
||||||
|
_uploadedDocs.remove(label);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
@ -89,6 +368,39 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showFacadePhotoOptionsForDoc(String docName) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Wrap(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.camera_alt, color: Color(0xFFAF101A)),
|
||||||
|
title: const Text('Take Photo'),
|
||||||
|
onTap: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
_handleImagePick(docName, ImageSource.camera);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.photo_library, color: Color(0xFFAF101A)),
|
||||||
|
title: const Text('Choose from Gallery'),
|
||||||
|
onTap: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
_handleImagePick(docName, ImageSource.gallery);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,6 +442,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close, size: 18, color: Colors.grey),
|
icon: const Icon(Icons.close, size: 18, color: Colors.grey),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedDocs.remove(docName);
|
_selectedDocs.remove(docName);
|
||||||
});
|
});
|
||||||
|
|
@ -140,6 +453,43 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Enter $docName Number (Optional if photo uploaded)',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
TextField(
|
||||||
|
controller: _getControllerForDoc(docName),
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'e.g. Enter $docName number',
|
||||||
|
prefixIcon: Icon(icon, size: 16, color: Colors.grey[500]),
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Divider(color: Colors.grey[300])),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Text(
|
||||||
|
'OR UPLOAD PHOTO',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: Divider(color: Colors.grey[300])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
if (isUploaded) ...[
|
if (isUploaded) ...[
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
|
|
@ -157,12 +507,19 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
color: Colors.red[50],
|
color: Colors.red[50],
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Center(
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Icon(
|
child: kIsWeb
|
||||||
Icons.insert_drive_file,
|
? Image.network(
|
||||||
color: Color(0xFFAF101A),
|
_uploadedDocs[docName]!.path,
|
||||||
size: 24,
|
fit: BoxFit.cover,
|
||||||
),
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
|
const Icon(Icons.broken_image, color: Color(0xFFAF101A)),
|
||||||
|
)
|
||||||
|
: Image.file(
|
||||||
|
File(_uploadedDocs[docName]!.path),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
|
const Icon(Icons.broken_image, color: Color(0xFFAF101A)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
@ -171,7 +528,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_uploadedDocs[docName] ?? 'Attached_document.pdf',
|
_uploadedDocs[docName]?.name ?? 'Attached_document.jpg',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -180,7 +537,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
const Text(
|
const Text(
|
||||||
'Size: 1.2 MB • Ready',
|
'Attached • Ready',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
|
|
@ -192,14 +549,14 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
setState(() {
|
setState(() {
|
||||||
_uploadedDocs.remove(docName);
|
_uploadedDocs.remove(docName);
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
SnackBar(
|
context,
|
||||||
content: Text('$docName attachment removed.'),
|
'$docName attachment removed.',
|
||||||
behavior: SnackBarBehavior.floating,
|
icon: Icons.delete_outline,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -239,19 +596,9 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
InteractiveScale(
|
||||||
onPressed: () {
|
child: ElevatedButton.icon(
|
||||||
setState(() {
|
onPressed: () => _handleImagePick(docName, ImageSource.camera),
|
||||||
_uploadedDocs[docName] = '${docName.toLowerCase().replaceAll(' ', '_')}_scan.jpg';
|
|
||||||
});
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('📸 Captured photo for $docName!'),
|
|
||||||
backgroundColor: const Color(0xFFAF101A),
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.camera_alt, size: 14),
|
icon: const Icon(Icons.camera_alt, size: 14),
|
||||||
label: const Text('Take Photo'),
|
label: const Text('Take Photo'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
@ -261,24 +608,14 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(16)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
OutlinedButton.icon(
|
InteractiveScale(
|
||||||
onPressed: () {
|
child: OutlinedButton.icon(
|
||||||
setState(() {
|
onPressed: () => _handleImagePick(docName, ImageSource.gallery),
|
||||||
_uploadedDocs[docName] = 'gallery_${docName.toLowerCase().replaceAll(' ', '_')}.png';
|
|
||||||
});
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('📂 Uploaded $docName from gallery!'),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.photo_library, size: 14),
|
icon: const Icon(Icons.photo_library, size: 14),
|
||||||
label: const Text('From Gallery'),
|
label: const Text('From Gallery'),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
|
|
@ -286,7 +623,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
side: const BorderSide(color: Color(0xFFAF101A)),
|
side: const BorderSide(color: Color(0xFFAF101A)),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(16)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -312,7 +649,10 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
|
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'PozoApp',
|
'PozoApp',
|
||||||
|
|
@ -339,7 +679,8 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
child: Row(
|
child: Row(
|
||||||
children: List.generate(5, (index) {
|
children: List.generate(5, (index) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Container(
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
height: 6,
|
height: 6,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 2.0),
|
margin: const EdgeInsets.symmetric(horizontal: 2.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -363,6 +704,11 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SlideFadeEntrance(
|
||||||
|
delay: Duration.zero,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -377,9 +723,16 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
'Verify your business details to start selling.',
|
'Verify your business details to start selling.',
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Name Field Input
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 100),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Business/Shop Name *',
|
'Business/Shop Name *',
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
|
@ -395,9 +748,16 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
),
|
),
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Category selection Block
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 180),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Business Type *',
|
'Business Type *',
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
|
@ -406,8 +766,6 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
// Category custom grid (2x3)
|
|
||||||
GridView.builder(
|
GridView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
|
@ -422,8 +780,10 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
final cat = _categories[index];
|
final cat = _categories[index];
|
||||||
final isSelected = _selectedCategory == cat['id'];
|
final isSelected = _selectedCategory == cat['id'];
|
||||||
|
|
||||||
return InkWell(
|
return InteractiveScale(
|
||||||
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
AppFeedback.select();
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedCategory = cat['id'];
|
_selectedCategory = cat['id'];
|
||||||
});
|
});
|
||||||
|
|
@ -473,12 +833,20 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// GSTIN optional Input group with Verify button on right-side
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 260),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -505,22 +873,37 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _gstinController,
|
controller: _gstinController,
|
||||||
textCapitalization: TextCapitalization.characters,
|
textCapitalization: TextCapitalization.characters,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '15-DIGIT GST NUMBER',
|
hintText: '15-DIGIT GST NUMBER',
|
||||||
|
suffixIcon: _hasGstinInput
|
||||||
|
? Icon(
|
||||||
|
_isGstinValid ? Icons.check_circle : Icons.error_outline,
|
||||||
|
color: _isGstinValid ? Colors.green : Colors.red,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
|
style: const TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
ElevatedButton(
|
InteractiveScale(
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (_isGstinValid) {
|
||||||
const SnackBar(
|
AppFeedback.success();
|
||||||
content: Text('GSTIN Verified Successfully!'),
|
showTopAlert(
|
||||||
backgroundColor: Colors.green,
|
context,
|
||||||
behavior: SnackBarBehavior.floating,
|
'GSTIN Verified Successfully!',
|
||||||
),
|
icon: Icons.verified_outlined,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
AppFeedback.error();
|
||||||
|
showTopAlert(
|
||||||
|
context,
|
||||||
|
'Invalid GSTIN Format. Check 15-digit code.',
|
||||||
|
isError: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: const Size(0, 48),
|
minimumSize: const Size(0, 48),
|
||||||
|
|
@ -533,6 +916,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
),
|
),
|
||||||
child: const Text('Verify'),
|
child: const Text('Verify'),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
|
|
@ -540,9 +924,16 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
"Auto-fetches address & legal name on verification.",
|
"Auto-fetches address & legal name on verification.",
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[500]),
|
style: TextStyle(fontSize: 11, color: Colors.grey[500]),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Shop Location Detect
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 340),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Shop Location *',
|
'Shop Location *',
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
|
@ -551,16 +942,9 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
InkWell(
|
InteractiveScale(
|
||||||
onTap: () {
|
child: InkWell(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
onTap: _fetchLocationAndAddress,
|
||||||
const SnackBar(
|
|
||||||
content: Text('📍 Location detected: Sector 62, Noida, UP'),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
|
@ -617,9 +1001,34 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Shop Address *',
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: _addressController,
|
||||||
|
maxLines: 3,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Enter complete shop address',
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Identity/License Documents
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 420),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Identity / License Documents (Select all that apply)',
|
'Identity / License Documents (Select all that apply)',
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
|
@ -639,9 +1048,15 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
_buildDocChip('PAN', Icons.credit_card),
|
_buildDocChip('PAN', Icons.credit_card),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// Expanded Upload Sections
|
|
||||||
..._selectedDocs.map((doc) {
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 500),
|
||||||
|
child: Column(
|
||||||
|
children: _selectedDocs.map((doc) {
|
||||||
IconData docIcon = Icons.description;
|
IconData docIcon = Icons.description;
|
||||||
if (doc == 'GSTIN') docIcon = Icons.description;
|
if (doc == 'GSTIN') docIcon = Icons.description;
|
||||||
if (doc == 'FSSAI') docIcon = Icons.restaurant;
|
if (doc == 'FSSAI') docIcon = Icons.restaurant;
|
||||||
|
|
@ -649,9 +1064,16 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
if (doc == 'Udhyog Aadhar') docIcon = Icons.domain;
|
if (doc == 'Udhyog Aadhar') docIcon = Icons.domain;
|
||||||
if (doc == 'PAN') docIcon = Icons.credit_card;
|
if (doc == 'PAN') docIcon = Icons.credit_card;
|
||||||
return _buildUploadSection(doc, docIcon);
|
return _buildUploadSection(doc, docIcon);
|
||||||
}),
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Shop Facade Photo upload area
|
|
||||||
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 580),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Shop Facade Photo *',
|
'Shop Facade Photo *',
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
|
@ -660,15 +1082,12 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
InkWell(
|
InteractiveScale(
|
||||||
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
_showFacadePhotoOptions();
|
||||||
const SnackBar(
|
|
||||||
content: Text('📸 Camera / Photo Upload Clicked!'),
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
|
|
@ -676,7 +1095,8 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: theme.colorScheme.outlineVariant,
|
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green : theme.colorScheme.outlineVariant,
|
||||||
|
width: _uploadedDocs.containsKey('facade_photo') ? 2.0 : 1.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -685,18 +1105,18 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.primary.withOpacity(0.1),
|
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green.withOpacity(0.1) : theme.colorScheme.primary.withOpacity(0.1),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.add_a_photo_outlined,
|
_uploadedDocs.containsKey('facade_photo') ? Icons.check_circle : Icons.add_a_photo_outlined,
|
||||||
color: theme.colorScheme.primary,
|
color: _uploadedDocs.containsKey('facade_photo') ? Colors.green : theme.colorScheme.primary,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'Upload front view',
|
_uploadedDocs.containsKey('facade_photo') ? _uploadedDocs['facade_photo']!.name : 'Upload front view',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
|
|
@ -704,7 +1124,7 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Clear photo showing shop board',
|
_uploadedDocs.containsKey('facade_photo') ? 'Facade Photo Attached • Tap to change' : 'Clear photo showing shop board',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
|
|
@ -714,6 +1134,10 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -721,16 +1145,21 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Button docked
|
SlideFadeEntrance(
|
||||||
Padding(
|
delay: const Duration(milliseconds: 660),
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||||
|
child: InteractiveScale(
|
||||||
|
hoverScale: _isNameValid ? 1.03 : 1.0,
|
||||||
|
tapScale: _isNameValid ? 0.96 : 1.0,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isNameValid
|
onPressed: _isNameValid
|
||||||
? () {
|
? () {
|
||||||
|
AppFeedback.click();
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
FadeSlidePageRoute(
|
||||||
builder: (context) => CustomizeSetupScreen(
|
child: CustomizeSetupScreen(
|
||||||
businessName: _nameController.text.trim(),
|
businessName: _nameController.text.trim(),
|
||||||
businessType: _selectedCategory,
|
businessType: _selectedCategory,
|
||||||
gstin: _gstinController.text.trim(),
|
gstin: _gstinController.text.trim(),
|
||||||
|
|
@ -744,9 +1173,14 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
backgroundColor: _isNameValid ? theme.colorScheme.primary : Colors.grey[200],
|
backgroundColor: _isNameValid ? theme.colorScheme.primary : Colors.grey[200],
|
||||||
foregroundColor: _isNameValid ? Colors.white : Colors.grey[400],
|
foregroundColor: _isNameValid ? Colors.white : Colors.grey[400],
|
||||||
elevation: _isNameValid ? 2 : 0,
|
elevation: _isNameValid ? 2 : 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Text('Continue to Verification'),
|
child: const Text('Continue to Verification'),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -805,7 +1239,6 @@ class DashedBorderPainter extends CustomPainter {
|
||||||
|
|
||||||
canvas.drawPath(dashPath, paint);
|
canvas.drawPath(dashPath, paint);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback for platform/renderers (such as Flutter Web HTML renderer) that do not support computeMetrics()
|
|
||||||
canvas.drawRRect(rrect, paint);
|
canvas.drawRRect(rrect, paint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
|
import '../route_transitions.dart';
|
||||||
|
import '../feedback_helper.dart';
|
||||||
import 'setup_complete_screen.dart';
|
import 'setup_complete_screen.dart';
|
||||||
|
|
||||||
class CustomizeSetupScreen extends StatefulWidget {
|
class CustomizeSetupScreen extends StatefulWidget {
|
||||||
|
|
@ -32,7 +34,10 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
|
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
title: Row(
|
title: Row(
|
||||||
children: const [
|
children: const [
|
||||||
|
|
@ -51,10 +56,11 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
FadeSlidePageRoute(
|
||||||
builder: (context) => SetupCompleteScreen(
|
child: SetupCompleteScreen(
|
||||||
businessName: widget.businessName,
|
businessName: widget.businessName,
|
||||||
businessType: widget.businessType,
|
businessType: widget.businessType,
|
||||||
gstin: widget.gstin,
|
gstin: widget.gstin,
|
||||||
|
|
@ -115,6 +121,11 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SlideFadeEntrance(
|
||||||
|
delay: Duration.zero,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -129,10 +140,15 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
'Turn on the features you need for your shop. You can always change these later in settings.',
|
'Turn on the features you need for your shop. You can always change these later in settings.',
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Toggle 1: Barcode
|
// Toggle 1: Barcode
|
||||||
_buildToggleCard(
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 100),
|
||||||
|
child: _buildToggleCard(
|
||||||
Icons.qr_code_scanner,
|
Icons.qr_code_scanner,
|
||||||
'Barcode Scanner',
|
'Barcode Scanner',
|
||||||
'Do you use a Barcode Scanner?',
|
'Do you use a Barcode Scanner?',
|
||||||
|
|
@ -140,10 +156,13 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
(val) => setState(() => _barcode = val),
|
(val) => setState(() => _barcode = val),
|
||||||
theme,
|
theme,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Toggle 2: KOT
|
// Toggle 2: KOT
|
||||||
_buildToggleCard(
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 180),
|
||||||
|
child: _buildToggleCard(
|
||||||
Icons.receipt_long,
|
Icons.receipt_long,
|
||||||
'KOT Management',
|
'KOT Management',
|
||||||
'Do you need Kitchen Order Ticket management?',
|
'Do you need Kitchen Order Ticket management?',
|
||||||
|
|
@ -151,10 +170,13 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
(val) => setState(() => _kot = val),
|
(val) => setState(() => _kot = val),
|
||||||
theme,
|
theme,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Toggle 3: Inventory stock tracking
|
// Toggle 3: Inventory stock tracking
|
||||||
_buildToggleCard(
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 260),
|
||||||
|
child: _buildToggleCard(
|
||||||
Icons.inventory_2,
|
Icons.inventory_2,
|
||||||
'Inventory Alerts',
|
'Inventory Alerts',
|
||||||
'Get notified when stock is running low.',
|
'Get notified when stock is running low.',
|
||||||
|
|
@ -162,6 +184,7 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
(val) => setState(() => _inventory = val),
|
(val) => setState(() => _inventory = val),
|
||||||
theme,
|
theme,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -169,14 +192,18 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
),
|
),
|
||||||
|
|
||||||
// Pinned action keys
|
// Pinned action keys
|
||||||
Padding(
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 340),
|
||||||
|
child: Padding(
|
||||||
padding: EdgeInsets.all(padding),
|
padding: EdgeInsets.all(padding),
|
||||||
|
child: InteractiveScale(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
FadeSlidePageRoute(
|
||||||
builder: (context) => SetupCompleteScreen(
|
child: SetupCompleteScreen(
|
||||||
businessName: widget.businessName,
|
businessName: widget.businessName,
|
||||||
businessType: widget.businessType,
|
businessType: widget.businessType,
|
||||||
gstin: widget.gstin,
|
gstin: widget.gstin,
|
||||||
|
|
@ -189,9 +216,14 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(54),
|
minimumSize: const Size.fromHeight(54),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Text('Save & Continue'),
|
child: const Text('Save & Continue'),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -209,7 +241,14 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
ValueChanged<bool> onChanged,
|
ValueChanged<bool> onChanged,
|
||||||
ThemeData theme,
|
ThemeData theme,
|
||||||
) {
|
) {
|
||||||
return Container(
|
void wrappedOnChanged(bool val) {
|
||||||
|
AppFeedback.click();
|
||||||
|
onChanged(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return InteractiveScale(
|
||||||
|
onTap: () => wrappedOnChanged(!value),
|
||||||
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|
@ -262,12 +301,13 @@ class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
|
||||||
),
|
),
|
||||||
Switch(
|
Switch(
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: onChanged,
|
onChanged: wrappedOnChanged,
|
||||||
activeColor: Colors.white,
|
activeColor: Colors.white,
|
||||||
activeTrackColor: theme.colorScheme.primary,
|
activeTrackColor: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,45 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
|
import '../route_transitions.dart';
|
||||||
|
import '../feedback_helper.dart';
|
||||||
import 'otp_verification_screen.dart';
|
import 'otp_verification_screen.dart';
|
||||||
|
|
||||||
|
class MockContact {
|
||||||
|
final String? displayName;
|
||||||
|
final List<MockPhone> phones;
|
||||||
|
|
||||||
|
MockContact({this.displayName, required String phoneNumber})
|
||||||
|
: phones = [MockPhone(phoneNumber)];
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockPhone {
|
||||||
|
final String number;
|
||||||
|
MockPhone(this.number);
|
||||||
|
}
|
||||||
|
|
||||||
class MobileLoginScreen extends StatefulWidget {
|
class MobileLoginScreen extends StatefulWidget {
|
||||||
const MobileLoginScreen({super.key});
|
final String? autoFilledMobileNumber;
|
||||||
|
|
||||||
|
const MobileLoginScreen({super.key, this.autoFilledMobileNumber});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MobileLoginScreen> createState() => _MobileLoginScreenState();
|
State<MobileLoginScreen> createState() => _MobileLoginScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
final TextEditingController _controller = TextEditingController();
|
late final TextEditingController _controller;
|
||||||
bool _isValid = false;
|
bool _isValid = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: widget.autoFilledMobileNumber ?? '');
|
||||||
|
_isValid = _controller.text.length == 10;
|
||||||
_controller.addListener(_validateInput);
|
_controller.addListener(_validateInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,6 +55,180 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _pickContact() async {
|
||||||
|
AppFeedback.click();
|
||||||
|
const permission = Permission.contacts;
|
||||||
|
var status = await permission.status;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (status.isDenied) {
|
||||||
|
final bool? proceed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Contacts Permission Required'),
|
||||||
|
content: const Text(
|
||||||
|
'PozoApp requires access to your contacts to quickly auto-fill mobile numbers during registration/billing.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: const Text('Continue'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
if (proceed != true) return;
|
||||||
|
status = await permission.request();
|
||||||
|
if (!mounted) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.isGranted || kIsWeb) {
|
||||||
|
_showContactPicker();
|
||||||
|
} else {
|
||||||
|
showTopAlert(
|
||||||
|
context,
|
||||||
|
'Contacts permission was denied. Please enter the number manually or enable it in Settings.',
|
||||||
|
isError: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<dynamic>> _loadContacts() async {
|
||||||
|
if (kIsWeb) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 600));
|
||||||
|
return [
|
||||||
|
MockContact(displayName: 'Amit Sharma', phoneNumber: '9876543210'),
|
||||||
|
MockContact(displayName: 'Rajesh Patel', phoneNumber: '9823456789'),
|
||||||
|
MockContact(displayName: 'Priya Sundaram', phoneNumber: '8765432109'),
|
||||||
|
MockContact(displayName: 'Karan Malhotra', phoneNumber: '7654321098'),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
final List<Contact> list = await FlutterContacts.getContacts(withProperties: true, withPhoto: false);
|
||||||
|
return list;
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectContactNumber(String? rawNumber) {
|
||||||
|
if (rawNumber == null) return;
|
||||||
|
var digits = rawNumber.replaceAll(RegExp(r'\D'), '');
|
||||||
|
if (digits.length == 12 && digits.startsWith('91')) {
|
||||||
|
digits = digits.substring(2);
|
||||||
|
}
|
||||||
|
if (digits.length == 11 && digits.startsWith('0')) {
|
||||||
|
digits = digits.substring(1);
|
||||||
|
}
|
||||||
|
if (digits.length > 10) {
|
||||||
|
digits = digits.substring(digits.length - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_controller.text = digits;
|
||||||
|
_isValid = digits.length == 10;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showContactPicker() async {
|
||||||
|
List<dynamic> allContacts = [];
|
||||||
|
List<dynamic> filteredContacts = [];
|
||||||
|
bool isLoading = true;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) {
|
||||||
|
if (isLoading) {
|
||||||
|
_loadContacts().then((loaded) {
|
||||||
|
setDialogState(() {
|
||||||
|
allContacts = loaded;
|
||||||
|
filteredContacts = loaded;
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Select Contact'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
height: 400,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Search contacts...',
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
),
|
||||||
|
onChanged: (query) {
|
||||||
|
setDialogState(() {
|
||||||
|
filteredContacts = allContacts.where((contact) {
|
||||||
|
final name = (contact.displayName ?? '').toLowerCase();
|
||||||
|
final phone = (contact.phones.isNotEmpty ? contact.phones.first.number : '').toLowerCase();
|
||||||
|
return name.contains(query.toLowerCase()) || phone.contains(query.toLowerCase());
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Expanded(
|
||||||
|
child: isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: filteredContacts.isEmpty
|
||||||
|
? const Center(child: Text('No contacts found'))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredContacts.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final contact = filteredContacts[index];
|
||||||
|
final name = contact.displayName ?? 'Unknown';
|
||||||
|
final phone = contact.phones.isNotEmpty ? contact.phones.first.number : 'No number';
|
||||||
|
return ListTile(
|
||||||
|
title: Text(name),
|
||||||
|
subtitle: Text(phone),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: const Color(0xFFAF101A).withOpacity(0.1),
|
||||||
|
child: Text(
|
||||||
|
name.isNotEmpty ? name[0].toUpperCase() : '?',
|
||||||
|
style: const TextStyle(color: Color(0xFFAF101A), fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
_selectContactNumber(phone);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Close'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
@ -41,7 +238,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Color(0xFFAF101A)),
|
icon: const Icon(Icons.arrow_back, color: Color(0xFFAF101A)),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'PozoApp',
|
'PozoApp',
|
||||||
|
|
@ -70,7 +270,11 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 1. Progress Bar: Step 2 of 5 with "Mobile Login" label on right
|
SlideFadeEntrance(
|
||||||
|
delay: Duration.zero,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -111,8 +315,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Title Header & Secure Badge Group
|
|
||||||
Text(
|
Text(
|
||||||
'Welcome Back',
|
'Welcome Back',
|
||||||
style: theme.textTheme.headlineLarge?.copyWith(
|
style: theme.textTheme.headlineLarge?.copyWith(
|
||||||
|
|
@ -124,10 +326,14 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
'Enter your mobile number to sign in or create a new account.',
|
'Enter your mobile number to sign in or create a new account.',
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 6. Top Badge: Green "Secure OTP Verification" chip with lock icon
|
SlideFadeEntrance(
|
||||||
Container(
|
delay: const Duration(milliseconds: 100),
|
||||||
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFAFFD1).withOpacity(0.8),
|
color: const Color(0xFFFAFFD1).withOpacity(0.8),
|
||||||
|
|
@ -149,14 +355,16 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 2. Truecaller Card
|
SlideFadeEntrance(
|
||||||
Container(
|
delay: const Duration(milliseconds: 200),
|
||||||
|
child: Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF3F3F3), // light gray background (surface low)
|
color: const Color(0xFFF3F3F3),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: const Color(0xFFE4BEBA).withOpacity(0.5)),
|
border: Border.all(color: const Color(0xFFE4BEBA).withOpacity(0.5)),
|
||||||
),
|
),
|
||||||
|
|
@ -172,7 +380,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.verified_user, // blue shield icon
|
Icons.verified_user,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
|
|
@ -215,21 +423,20 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
_controller.text = '9876543210';
|
_controller.text = '9876543210';
|
||||||
setState(() {
|
setState(() {
|
||||||
_isValid = true;
|
_isValid = true;
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('⚡ Phone verified automatically with Truecaller! Joining PozoApp...'),
|
'Phone verified automatically with Truecaller! Joining PozoApp...',
|
||||||
backgroundColor: Color(0xFF005FAF),
|
icon: Icons.verified_user_outlined,
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
FadeSlidePageRoute(
|
||||||
builder: (context) => const OtpVerificationScreen(
|
child: const OtpVerificationScreen(
|
||||||
mobileNumber: '9876543210',
|
mobileNumber: '9876543210',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -249,7 +456,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
minimumSize: const Size.fromHeight(56),
|
minimumSize: const Size.fromHeight(56),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8), // 8px for buttons
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
),
|
),
|
||||||
|
|
@ -266,10 +473,15 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
SizedBox(height: gap),
|
SizedBox(height: gap),
|
||||||
|
|
||||||
// 3. Divider: "Or enter mobile number manually"
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 300),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
|
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
|
||||||
|
|
@ -287,9 +499,7 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
|
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: gap),
|
SizedBox(height: gap),
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
'Mobile Number',
|
'Mobile Number',
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
|
@ -298,8 +508,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 4. Mobile Number field: Country code group on left, number field on right
|
|
||||||
Container(
|
Container(
|
||||||
height: 56,
|
height: 56,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -316,7 +524,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Flag and code prefix box
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -326,7 +533,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Indian flag representation
|
|
||||||
Container(
|
Container(
|
||||||
width: 22,
|
width: 22,
|
||||||
height: 14,
|
height: 14,
|
||||||
|
|
@ -365,8 +571,6 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Input field
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
|
@ -388,7 +592,14 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.contact_phone, color: Color(0xFFAF101A)),
|
||||||
|
onPressed: _pickContact,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -398,18 +609,22 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 5. Bottom buttons
|
SlideFadeEntrance(
|
||||||
Column(
|
delay: const Duration(milliseconds: 400),
|
||||||
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Full width red "Send OTP" button
|
InteractiveScale(
|
||||||
ElevatedButton(
|
hoverScale: _isValid ? 1.03 : 1.0,
|
||||||
|
tapScale: _isValid ? 0.96 : 1.0,
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: _isValid
|
onPressed: _isValid
|
||||||
? () {
|
? () {
|
||||||
|
AppFeedback.click();
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
FadeSlidePageRoute(
|
||||||
builder: (context) => OtpVerificationScreen(
|
child: OtpVerificationScreen(
|
||||||
mobileNumber: _controller.text,
|
mobileNumber: _controller.text,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -422,27 +637,43 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
foregroundColor: _isValid ? Colors.white : Colors.grey[400],
|
foregroundColor: _isValid ? Colors.white : Colors.grey[400],
|
||||||
elevation: _isValid ? 2 : 0,
|
elevation: _isValid ? 2 : 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8), // 8px default button radius
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text('Send OTP'),
|
child: const Text('Send OTP'),
|
||||||
),
|
),
|
||||||
SizedBox(height: gap / 2),
|
|
||||||
// Outline style red "Help / Talk to Support" button with chat icon
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed: () {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('💬 Connecting with Pozo Support... Please wait.'),
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
),
|
||||||
|
SizedBox(height: gap / 2),
|
||||||
|
InteractiveScale(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
AppFeedback.click();
|
||||||
|
final Uri uri = Uri.parse('tel:7324000011');
|
||||||
|
try {
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri);
|
||||||
|
} else {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
showTopAlert(
|
||||||
|
context,
|
||||||
|
'Could not launch dialer. Call 7324000011',
|
||||||
|
isError: true,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
showTopAlert(
|
||||||
|
context,
|
||||||
|
'Dialer not available in this environment. Call 7324000011',
|
||||||
|
isError: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(56),
|
minimumSize: const Size.fromHeight(56),
|
||||||
side: const BorderSide(color: Color(0xFFAF101A), width: 2),
|
side: const BorderSide(color: Color(0xFFAF101A), width: 2),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8), // 8px default button radius
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -461,8 +692,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
|
import '../route_transitions.dart';
|
||||||
|
import '../feedback_helper.dart';
|
||||||
import 'business_setup_screen.dart';
|
import 'business_setup_screen.dart';
|
||||||
|
|
||||||
class OtpVerificationScreen extends StatefulWidget {
|
class OtpVerificationScreen extends StatefulWidget {
|
||||||
|
|
@ -49,6 +51,21 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _verifyOtp() {
|
||||||
|
final code = _controllers.map((c) => c.text.trim()).join();
|
||||||
|
if (code.length < 6) {
|
||||||
|
AppFeedback.error();
|
||||||
|
showTopAlert(context, 'Please enter a valid 6-digit OTP code.', isError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AppFeedback.success();
|
||||||
|
showTopAlert(context, 'OTP Verified Successfully!');
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
FadeSlidePageRoute(child: const BusinessSetupScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String _getFormattedTime() {
|
String _getFormattedTime() {
|
||||||
return '00:${_timeLeft.toString().padLeft(2, '0')}';
|
return '00:${_timeLeft.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +83,10 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
|
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'PozoApp',
|
'PozoApp',
|
||||||
|
|
@ -122,8 +142,10 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Alert envelope pulse symbol
|
// Alert envelope pulse symbol with SlideFadeEntrance
|
||||||
Center(
|
SlideFadeEntrance(
|
||||||
|
delay: Duration.zero,
|
||||||
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 96,
|
width: 96,
|
||||||
height: 96,
|
height: 96,
|
||||||
|
|
@ -154,16 +176,22 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 100),
|
||||||
|
child: Text(
|
||||||
'Verifying your number',
|
'Verifying your number',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: theme.textTheme.headlineMedium?.copyWith(
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 150),
|
||||||
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -172,8 +200,11 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
GestureDetector(
|
InteractiveScale(
|
||||||
onTap: () => Navigator.pop(context),
|
onTap: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'Edit',
|
'Edit',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
@ -185,11 +216,14 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// 6 Custom Inputs Fields
|
// 6 Custom Inputs Fields with SlideFadeEntrance
|
||||||
Row(
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 200),
|
||||||
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: List.generate(11, (index) {
|
children: List.generate(11, (index) {
|
||||||
if (index.isOdd) {
|
if (index.isOdd) {
|
||||||
|
|
@ -212,32 +246,42 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
counterText: '',
|
counterText: '',
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: theme.colorScheme.outlineVariant),
|
borderSide: BorderSide(color: theme.colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: theme.colorScheme.primary, width: 2),
|
borderSide: BorderSide(color: theme.colorScheme.primary, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value.isNotEmpty && fieldIndex < 5) {
|
AppFeedback.subtleHaptic();
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
if (fieldIndex < 5) {
|
||||||
_focusNodes[fieldIndex + 1].requestFocus();
|
_focusNodes[fieldIndex + 1].requestFocus();
|
||||||
|
} else {
|
||||||
|
_verifyOtp();
|
||||||
|
}
|
||||||
|
} else if (value.isEmpty && fieldIndex > 0) {
|
||||||
|
_focusNodes[fieldIndex - 1].requestFocus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Auto detect loader
|
// Auto detect loader card with SlideFadeEntrance
|
||||||
Container(
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 250),
|
||||||
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceContainerLow,
|
color: theme.colorScheme.surfaceContainerLow,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|
@ -260,11 +304,14 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Resending timer action column
|
// Resending timer action row
|
||||||
Row(
|
SlideFadeEntrance(
|
||||||
|
delay: const Duration(milliseconds: 300),
|
||||||
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.schedule, size: 16, color: Colors.grey),
|
const Icon(Icons.schedule, size: 16, color: Colors.grey),
|
||||||
|
|
@ -277,29 +324,41 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// WhatsApp custom CTA button
|
// WhatsApp custom CTA button with InteractiveScale
|
||||||
ElevatedButton(
|
SlideFadeEntrance(
|
||||||
onPressed: () {},
|
delay: const Duration(milliseconds: 350),
|
||||||
style: ElevatedButton.styleFrom(
|
child: InteractiveScale(
|
||||||
backgroundColor: Colors.white,
|
child: OutlinedButton.icon(
|
||||||
foregroundColor: Colors.black87,
|
onPressed: () {
|
||||||
side: const BorderSide(color: Color(0xFFE2E2E2)),
|
AppFeedback.click();
|
||||||
elevation: 1,
|
showTopAlert(
|
||||||
minimumSize: const Size(200, 48),
|
context,
|
||||||
),
|
'WhatsApp verification request sent!',
|
||||||
child: Row(
|
icon: Icons.chat_bubble_outline,
|
||||||
mainAxisSize: MainAxisSize.min,
|
);
|
||||||
children: const [
|
},
|
||||||
Text(
|
icon: const Icon(Icons.forum, color: Colors.green, size: 18),
|
||||||
|
label: const Text(
|
||||||
'Resend via WhatsApp',
|
'Resend via WhatsApp',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
side: const BorderSide(color: Color(0xFFE2E2E2)),
|
||||||
|
elevation: 1,
|
||||||
|
minimumSize: const Size(220, 48),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
@ -325,17 +384,23 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
_buildFooterAction(Icons.help_outline, 'Help'),
|
_buildFooterAction(Icons.help_outline, 'Help', () {
|
||||||
_buildFooterAction(Icons.save, 'Save Draft'),
|
showTopAlert(
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
'Support agent contacted. We will call you shortly.',
|
||||||
builder: (context) => const BusinessSetupScreen(),
|
icon: Icons.support_agent_outlined,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
|
_buildFooterAction(Icons.save, 'Save Draft', () {
|
||||||
|
showTopAlert(
|
||||||
|
context,
|
||||||
|
'Onboarding draft saved successfully!',
|
||||||
|
icon: Icons.save_outlined,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
InteractiveScale(
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: _verifyOtp,
|
||||||
icon: const Text(
|
icon: const Text(
|
||||||
'Verify Manually',
|
'Verify Manually',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
|
@ -346,7 +411,8 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
backgroundColor: theme.colorScheme.primary,
|
backgroundColor: theme.colorScheme.primary,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -362,8 +428,13 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFooterAction(IconData icon, String label) {
|
Widget _buildFooterAction(IconData icon, String label, VoidCallback onTap) {
|
||||||
return Column(
|
return InteractiveScale(
|
||||||
|
onTap: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
onTap();
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: Colors.grey[600], size: 22),
|
Icon(icon, color: Colors.grey[600], size: 22),
|
||||||
|
|
@ -377,6 +448,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
|
import '../route_transitions.dart';
|
||||||
|
import '../feedback_helper.dart';
|
||||||
import 'billing_dashboard_screen.dart';
|
import 'billing_dashboard_screen.dart';
|
||||||
|
|
||||||
class SetupCompleteScreen extends StatelessWidget {
|
class SetupCompleteScreen extends StatefulWidget {
|
||||||
final String businessName;
|
final String businessName;
|
||||||
final String businessType;
|
final String businessType;
|
||||||
final String gstin;
|
final String gstin;
|
||||||
|
|
@ -20,6 +22,73 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
this.inventoryAlerts = true,
|
this.inventoryAlerts = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SetupCompleteScreen> createState() => _SetupCompleteScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SetupCompleteScreenState extends State<SetupCompleteScreen> with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _checkScaleAnimation;
|
||||||
|
late Animation<double> _cardSlideAnimation;
|
||||||
|
late Animation<double> _cardOpacityAnimation;
|
||||||
|
late Animation<double> _btnSlideAnimation;
|
||||||
|
late Animation<double> _btnOpacityAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1400),
|
||||||
|
);
|
||||||
|
|
||||||
|
_checkScaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.0, 0.45, curve: Curves.elasticOut),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_cardSlideAnimation = Tween<double>(begin: 32.0, end: 0.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.25, 0.75, curve: Curves.easeOutCubic),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_cardOpacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.25, 0.65, curve: Curves.easeIn),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_btnSlideAnimation = Tween<double>(begin: 24.0, end: 0.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.55, 1.0, curve: Curves.easeOutCubic),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_btnOpacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.55, 0.85, curve: Curves.easeIn),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_controller.forward();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
AppFeedback.success();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
String _getCategoryDisplayName(String id) {
|
String _getCategoryDisplayName(String id) {
|
||||||
final cleanId = id.replaceAll('category/', '').trim().toLowerCase();
|
final cleanId = id.replaceAll('category/', '').trim().toLowerCase();
|
||||||
switch (cleanId) {
|
switch (cleanId) {
|
||||||
|
|
@ -40,7 +109,7 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final categoryText = _getCategoryDisplayName(businessType);
|
final categoryText = _getCategoryDisplayName(widget.businessType);
|
||||||
final gap = context.responsiveGap;
|
final gap = context.responsiveGap;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -81,8 +150,10 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Circular Green Check badge
|
// Circular Green Check badge with Elastic Entrance Animation
|
||||||
Container(
|
ScaleTransition(
|
||||||
|
scale: _checkScaleAnimation,
|
||||||
|
child: Container(
|
||||||
width: 96,
|
width: 96,
|
||||||
height: 96,
|
height: 96,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
|
|
@ -112,6 +183,7 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
SizedBox(height: gap),
|
SizedBox(height: gap),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -130,8 +202,19 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
|
|
||||||
SizedBox(height: gap * 1.5),
|
SizedBox(height: gap * 1.5),
|
||||||
|
|
||||||
// Glassmorphism summary Card
|
// Glassmorphism summary Card with Fade + Slide Transition
|
||||||
Container(
|
AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.translate(
|
||||||
|
offset: Offset(0.0, _cardSlideAnimation.value),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _cardOpacityAnimation.value,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -176,7 +259,7 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
businessName,
|
widget.businessName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -267,10 +350,10 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
[
|
[
|
||||||
if (barcodeScanner) 'Barcode Enabled',
|
if (widget.barcodeScanner) 'Barcode Enabled',
|
||||||
if (kotManagement) 'KOT/Kitchen Order',
|
if (widget.kotManagement) 'KOT/Kitchen Order',
|
||||||
if (inventoryAlerts) 'Low Stock Warnings',
|
if (widget.inventoryAlerts) 'Low Stock Warnings',
|
||||||
if (!barcodeScanner && !kotManagement && !inventoryAlerts) 'Standard Register',
|
if (!widget.barcodeScanner && !widget.kotManagement && !widget.inventoryAlerts) 'Standard Register',
|
||||||
].join(', '),
|
].join(', '),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|
@ -286,30 +369,41 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Primary Launcher CTA
|
// Primary Launcher CTA with Slide + Fade + Hover/Tap Scale micro-animations
|
||||||
ElevatedButton(
|
AnimatedBuilder(
|
||||||
onPressed: () {
|
animation: _controller,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
builder: (context, child) {
|
||||||
const SnackBar(
|
return Transform.translate(
|
||||||
content: Text('⚡ Launching Register Dashboard... Enjoy billing!'),
|
offset: Offset(0.0, _btnSlideAnimation.value),
|
||||||
behavior: SnackBarBehavior.floating,
|
child: Opacity(
|
||||||
backgroundColor: Colors.green,
|
opacity: _btnOpacityAnimation.value,
|
||||||
duration: Duration(milliseconds: 1500),
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
child: InteractiveScale(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
showTopAlert(
|
||||||
|
context,
|
||||||
|
'Launching Register Dashboard... Enjoy billing!',
|
||||||
|
icon: Icons.rocket_launch_outlined,
|
||||||
|
);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
FadeSlidePageRoute(
|
||||||
builder: (context) => BillingDashboardScreen(
|
child: BillingDashboardScreen(
|
||||||
businessName: businessName,
|
businessName: widget.businessName,
|
||||||
businessType: businessType,
|
businessType: widget.businessType,
|
||||||
gstin: gstin,
|
gstin: widget.gstin,
|
||||||
barcodeScanner: barcodeScanner,
|
barcodeScanner: widget.barcodeScanner,
|
||||||
kotManagement: kotManagement,
|
kotManagement: widget.kotManagement,
|
||||||
inventoryAlerts: inventoryAlerts,
|
inventoryAlerts: widget.inventoryAlerts,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -317,7 +411,7 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(56),
|
minimumSize: const Size.fromHeight(56),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -329,6 +423,8 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -338,5 +434,5 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,197 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:mobile_number/mobile_number.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
|
import '../route_transitions.dart';
|
||||||
|
import '../feedback_helper.dart';
|
||||||
import 'mobile_login_screen.dart';
|
import 'mobile_login_screen.dart';
|
||||||
|
|
||||||
class WelcomeScreen extends StatelessWidget {
|
class WelcomeScreen extends StatefulWidget {
|
||||||
const WelcomeScreen({super.key});
|
const WelcomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WelcomeScreen> createState() => _WelcomeScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WelcomeScreenState extends State<WelcomeScreen> with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _splashScaleAnimation;
|
||||||
|
late Animation<double> _splashLogoOpacityAnimation;
|
||||||
|
late Animation<double> _splashContainerOpacityAnimation;
|
||||||
|
late Animation<double> _contentFadeAnimation;
|
||||||
|
late Animation<Offset> _contentSlideAnimation;
|
||||||
|
|
||||||
|
String? _detectedMobileNumber;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initMobileNumberDetection();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1400),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Splash Logo Zoom/Scale-down (from 3.0 to 1.0)
|
||||||
|
_splashScaleAnimation = Tween<double>(begin: 3.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.0, 0.45, curve: Curves.easeOutBack),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Splash Logo Fade-in as it scales down
|
||||||
|
_splashLogoOpacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.0, 0.35, curve: Curves.easeIn),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Splash Container Fade-out
|
||||||
|
_splashContainerOpacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.45, 0.6, curve: Curves.easeOut),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Welcome Content Fade-in
|
||||||
|
_contentFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.55, 1.0, curve: Curves.easeIn),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Welcome Content Slide-up
|
||||||
|
_contentSlideAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(0.0, 0.05),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: const Interval(0.55, 1.0, curve: Curves.easeOutCubic),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_controller.forward();
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
if (mounted) {
|
||||||
|
AppFeedback.success();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initMobileNumberDetection() async {
|
||||||
|
try {
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
if (await MobileNumber.hasPhonePermission) {
|
||||||
|
final String? mobileNumber = await MobileNumber.mobileNumber;
|
||||||
|
if (mobileNumber != null && mobileNumber.isNotEmpty) {
|
||||||
|
// Extract 10 digits if possible, else just use the number
|
||||||
|
final cleaned = mobileNumber.replaceAll(RegExp(r'\D'), '');
|
||||||
|
if (cleaned.length >= 10) {
|
||||||
|
_detectedMobileNumber = cleaned.substring(cleaned.length - 10);
|
||||||
|
} else {
|
||||||
|
_detectedMobileNumber = mobileNumber;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error getting mobile number: $e');
|
||||||
|
}
|
||||||
|
// Fallback to mock number
|
||||||
|
_detectedMobileNumber = '9988776655';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final isMobile = context.isMobile;
|
final isMobile = context.isMobile;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
final showSplash = _controller.value < 0.6;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
// Main Welcome Content
|
||||||
|
Opacity(
|
||||||
|
opacity: _contentFadeAnimation.value,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _contentSlideAnimation,
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: showSplash,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Splash Overlay (Centered Logo scaling down)
|
||||||
|
if (showSplash)
|
||||||
|
Opacity(
|
||||||
|
opacity: _splashContainerOpacityAnimation.value,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.white,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: ScaleTransition(
|
||||||
|
scale: _splashScaleAnimation,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _splashLogoOpacityAnimation.value,
|
||||||
|
child: Container(
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.08),
|
||||||
|
blurRadius: 24,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/logo/pozo_logo.jpg',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Top Progress bar indicator - 1st segment active
|
// Top Progress bar indicator - 1st segment active + Quick Feedback Settings
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: List.generate(5, (index) {
|
children: List.generate(5, (index) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Container(
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
height: 4,
|
height: 4,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 2.0),
|
margin: const EdgeInsets.symmetric(horizontal: 2.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -32,6 +203,11 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildSettingsButton(theme),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Scrollable / Responsive Content
|
// Scrollable / Responsive Content
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -142,6 +318,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,50 +327,22 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: theme.colorScheme.primary.withOpacity(0.15),
|
color: Colors.black.withOpacity(0.12),
|
||||||
blurRadius: 24,
|
blurRadius: 20,
|
||||||
offset: const Offset(0, 8),
|
offset: const Offset(0, 6),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
child: ClipRRect(
|
||||||
child: Stack(
|
borderRadius: BorderRadius.circular(20),
|
||||||
clipBehavior: Clip.none,
|
child: Image.asset(
|
||||||
children: [
|
'assets/logo/pozo_logo.jpg',
|
||||||
Text(
|
fit: BoxFit.cover,
|
||||||
'Pozo',
|
|
||||||
style: theme.textTheme.headlineMedium?.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
letterSpacing: -1.0,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
|
||||||
top: -16,
|
|
||||||
right: -16,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.amber[700],
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'IN',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 8,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,156 +376,8 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMockDashboard(ThemeData theme) {
|
Widget _buildMockDashboard(ThemeData theme) {
|
||||||
return Container(
|
return const Center(
|
||||||
width: 280,
|
child: RetailUniverseVisual(),
|
||||||
height: 230,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.04),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 8),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
height: 44,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[50],
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
|
||||||
border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Container(
|
|
||||||
width: 70,
|
|
||||||
height: 8,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[300],
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 14,
|
|
||||||
height: 14,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[300],
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_buildMockRow(Icons.description, Colors.red[50]!, Colors.red),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
_buildMockRow(Icons.poll, Colors.green[50]!, Colors.green),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 60,
|
|
||||||
right: -12,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
border: Border.all(color: Colors.grey[200]!),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.06),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(2),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.green,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.check,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
const Text(
|
|
||||||
'Saved',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 30,
|
|
||||||
left: -16,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: theme.colorScheme.primary.withOpacity(0.2),
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, 6),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'₹ 4,500',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -385,11 +386,13 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: context.isMobile ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
crossAxisAlignment: context.isMobile ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton(
|
InteractiveScale(
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const MobileLoginScreen()),
|
FadeSlidePageRoute(child: MobileLoginScreen(autoFilledMobileNumber: _detectedMobileNumber)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
@ -405,6 +408,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: context.isMobile ? MainAxisAlignment.center : MainAxisAlignment.start,
|
mainAxisAlignment: context.isMobile ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||||
|
|
@ -441,52 +445,403 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMockRow(IconData icon, Color bg, Color color) {
|
Widget _buildSettingsButton(ThemeData theme) {
|
||||||
return Container(
|
final hasSound = AppFeedback.isSoundEnabled;
|
||||||
height: 48,
|
final hasHaptic = AppFeedback.isHapticEnabled;
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _showFeedbackSettings(theme),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: bg.withOpacity(0.4),
|
color: theme.colorScheme.surfaceContainerLow,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(color: Colors.grey[100]!),
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
Icon(
|
||||||
|
hasSound ? Icons.volume_up_rounded : Icons.volume_off_rounded,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Icon(
|
||||||
|
hasHaptic ? Icons.vibration_rounded : Icons.phone_android_rounded,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showFeedbackSettings(ThemeData theme) {
|
||||||
|
AppFeedback.click();
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
),
|
||||||
|
builder: (context) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setModalState) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Feedback Settings',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
AppFeedback.click();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
AppFeedback.isSoundEnabled ? Icons.volume_up : Icons.volume_off,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: const Text('Sound Effects', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
subtitle: const Text('Play subtle sounds on button clicks & selections'),
|
||||||
|
trailing: Switch(
|
||||||
|
value: AppFeedback.isSoundEnabled,
|
||||||
|
activeColor: theme.colorScheme.primary,
|
||||||
|
onChanged: (val) {
|
||||||
|
AppFeedback.isSoundEnabled = val;
|
||||||
|
AppFeedback.click();
|
||||||
|
setModalState(() {});
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
AppFeedback.isHapticEnabled ? Icons.vibration : Icons.mobile_off,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: const Text('Haptic Vibration', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
subtitle: const Text('Feel tactical vibrations on user interaction'),
|
||||||
|
trailing: Switch(
|
||||||
|
value: AppFeedback.isHapticEnabled,
|
||||||
|
activeColor: theme.colorScheme.primary,
|
||||||
|
onChanged: (val) {
|
||||||
|
AppFeedback.isHapticEnabled = val;
|
||||||
|
AppFeedback.click();
|
||||||
|
setModalState(() {});
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RetailUniverseVisual extends StatefulWidget {
|
||||||
|
const RetailUniverseVisual({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RetailUniverseVisual> createState() => _RetailUniverseVisualState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RetailUniverseVisualState extends State<RetailUniverseVisual> with TickerProviderStateMixin {
|
||||||
|
late AnimationController _orbitController;
|
||||||
|
late AnimationController _pulseController;
|
||||||
|
int _activeIndex = 0;
|
||||||
|
|
||||||
|
final List<Map<String, dynamic>> _retailUniverse = [
|
||||||
|
{
|
||||||
|
'title': 'Grocery & Retail',
|
||||||
|
'subtitle': 'Groceries, Wholesale, Dept. Stores, Xerox, Stationery',
|
||||||
|
'icon': Icons.shopping_basket_rounded,
|
||||||
|
'color': Colors.green,
|
||||||
|
'bgColor': const Color(0xFFE8F5E9),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Food & Beverages',
|
||||||
|
'subtitle': 'Restaurants, Bakeries, Dairy, Ice Cream, Coffee Shops',
|
||||||
|
'icon': Icons.restaurant_rounded,
|
||||||
|
'color': Colors.orange,
|
||||||
|
'bgColor': const Color(0xFFFFF3E0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Wellness & Beauty',
|
||||||
|
'subtitle': 'Salon, Spa, Beauty Parlours, Cosmetics, Fitness',
|
||||||
|
'icon': Icons.spa_rounded,
|
||||||
|
'color': Colors.pink,
|
||||||
|
'bgColor': const Color(0xFFFCE4EC),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Fashion & Lifestyle',
|
||||||
|
'subtitle': 'Fashion & Jewellery Boutique, Lifestyle, Footwear Stores',
|
||||||
|
'icon': Icons.checkroom_rounded,
|
||||||
|
'color': Colors.indigo,
|
||||||
|
'bgColor': const Color(0xFFE8EAF6),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Electronics & Tech',
|
||||||
|
'subtitle': 'Electricals, Computers, Mobiles, Appliances',
|
||||||
|
'icon': Icons.computer_rounded,
|
||||||
|
'color': Colors.blue,
|
||||||
|
'bgColor': const Color(0xFFE3F2FD),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Logistics & Healthcare',
|
||||||
|
'subtitle': 'PozoHealth, Smart Parking, Boating, Transport & Logistics',
|
||||||
|
'icon': Icons.local_hospital_rounded,
|
||||||
|
'color': Colors.teal,
|
||||||
|
'bgColor': const Color(0xFFE0F2F1),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_orbitController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(seconds: 30),
|
||||||
|
)..repeat();
|
||||||
|
|
||||||
|
_pulseController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1500),
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
|
||||||
|
_startAutoRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startAutoRotation() {
|
||||||
|
Future.delayed(const Duration(milliseconds: 3500), _nextCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _nextCategory() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_activeIndex = (_activeIndex + 1) % _retailUniverse.length;
|
||||||
|
});
|
||||||
|
_startAutoRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setActiveCategory(int index) {
|
||||||
|
AppFeedback.select();
|
||||||
|
setState(() {
|
||||||
|
_activeIndex = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_orbitController.dispose();
|
||||||
|
_pulseController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final activeData = _retailUniverse[_activeIndex];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 320,
|
||||||
|
height: 320,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
// 1. Orbital circular path
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(6),
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.grey[200]!,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 2. Rotating orbital icons
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _orbitController,
|
||||||
|
builder: (context, child) {
|
||||||
|
final double baseAngle = _orbitController.value * 2 * math.pi;
|
||||||
|
return Stack(
|
||||||
|
children: List.generate(_retailUniverse.length, (index) {
|
||||||
|
final double angle = baseAngle + (index * 2 * math.pi / _retailUniverse.length);
|
||||||
|
const double radius = 100.0;
|
||||||
|
final double x = radius * math.cos(angle);
|
||||||
|
final double y = radius * math.sin(angle);
|
||||||
|
final isCurrent = index == _activeIndex;
|
||||||
|
final item = _retailUniverse[index];
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: 160.0 + x - 24.0,
|
||||||
|
top: 160.0 + y - 24.0,
|
||||||
|
child: InteractiveScale(
|
||||||
|
onTap: () => _setActiveCategory(index),
|
||||||
|
child: Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isCurrent ? item['bgColor'] : Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: isCurrent ? item['color'] : Colors.grey[300]!,
|
||||||
|
width: isCurrent ? 2 : 1,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
item['icon'],
|
||||||
|
color: isCurrent ? item['color'] : Colors.grey[500],
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 3. Central active node with pulse animation
|
||||||
|
ScaleTransition(
|
||||||
|
scale: Tween<double>(begin: 0.95, end: 1.05).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _pulseController,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
width: 110,
|
||||||
|
height: 110,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: activeData['bgColor'],
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: activeData['color'],
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: activeData['color'].withOpacity(0.2),
|
||||||
|
blurRadius: 24,
|
||||||
|
spreadRadius: 4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
activeData['icon'],
|
||||||
|
color: activeData['color'],
|
||||||
|
size: 38,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 4. Floating category text label at the bottom
|
||||||
|
Positioned(
|
||||||
|
bottom: -32,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, animation) => FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.2),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(animation),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: KeyedSubtree(
|
||||||
|
key: ValueKey<int>(_activeIndex),
|
||||||
|
child: Container(
|
||||||
|
width: 280,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Colors.grey[100]!),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.04),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Icon(icon, color: color, size: 16),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Text(
|
||||||
width: 100,
|
activeData['title'],
|
||||||
height: 6,
|
textAlign: TextAlign.center,
|
||||||
decoration: BoxDecoration(
|
style: TextStyle(
|
||||||
color: Colors.grey[400],
|
fontSize: 14,
|
||||||
borderRadius: BorderRadius.circular(3),
|
fontWeight: FontWeight.bold,
|
||||||
|
color: activeData['color'],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 4),
|
||||||
Container(
|
Text(
|
||||||
width: 50,
|
activeData['subtitle'],
|
||||||
height: 4,
|
textAlign: TextAlign.center,
|
||||||
decoration: BoxDecoration(
|
maxLines: 2,
|
||||||
color: Colors.grey[300],
|
overflow: TextOverflow.ellipsis,
|
||||||
borderRadius: BorderRadius.circular(2),
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
height: 1.3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Icon(Icons.more_vert, color: Colors.grey[400], size: 16)
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ class PozoTheme {
|
||||||
minimumSize: const Size.fromHeight(54),
|
minimumSize: const Size.fromHeight(54),
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
textStyle: GoogleFonts.plusJakartaSans(
|
textStyle: GoogleFonts.plusJakartaSans(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.9"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -9,6 +25,62 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.0"
|
||||||
|
audioplayers:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: audioplayers
|
||||||
|
sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.2.1"
|
||||||
|
audioplayers_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_android
|
||||||
|
sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.3"
|
||||||
|
audioplayers_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_darwin
|
||||||
|
sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.2"
|
||||||
|
audioplayers_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_linux
|
||||||
|
sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
audioplayers_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_platform_interface
|
||||||
|
sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
|
audioplayers_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_web
|
||||||
|
sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
|
audioplayers_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_windows
|
||||||
|
sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -25,6 +97,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
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:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -41,6 +129,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -57,6 +153,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
dbus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dbus
|
||||||
|
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.14"
|
||||||
|
device_info_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus
|
||||||
|
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.5.0"
|
||||||
|
device_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_platform_interface
|
||||||
|
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.3"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -73,11 +193,83 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4+4"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+5"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_contacts:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_contacts
|
||||||
|
sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.9+2"
|
||||||
|
flutter_haptic_feedback:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_haptic_feedback
|
||||||
|
sha256: e6a1ac6b5119c9b0964e4de2eaa6bfb66facedc6e33bfe2a902da3f6f9666499
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
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:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
|
@ -86,11 +278,120 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.31"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
geoclue:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geoclue
|
||||||
|
sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.1"
|
||||||
|
geocoding:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: geocoding
|
||||||
|
sha256: "606be036287842d779d7ec4e2f6c9435fc29bbbd3c6da6589710f981d8852895"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
geocoding_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geocoding_android
|
||||||
|
sha256: ba810da90d6633cbb82bbab630e5b4a3b7d23503263c00ae7f1ef0316dcae5b9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.1"
|
||||||
|
geocoding_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geocoding_ios
|
||||||
|
sha256: "18ab1c8369e2b0dcb3a8ccc907319334f35ee8cf4cfef4d9c8e23b13c65cb825"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
geocoding_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geocoding_platform_interface
|
||||||
|
sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.0"
|
||||||
|
geolocator:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: geolocator
|
||||||
|
sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.0.2"
|
||||||
|
geolocator_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_android
|
||||||
|
sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.2"
|
||||||
|
geolocator_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_apple
|
||||||
|
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.13"
|
||||||
|
geolocator_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_linux
|
||||||
|
sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.4"
|
||||||
|
geolocator_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_platform_interface
|
||||||
|
sha256: dde05dae7d584db6e82feb87dd9fb0b4b4c83ed68065667b4bef637be38e13a7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.7"
|
||||||
|
geolocator_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_web
|
||||||
|
sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.3"
|
||||||
|
geolocator_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_windows
|
||||||
|
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.5"
|
||||||
google_fonts:
|
google_fonts:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -99,6 +400,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.1.0"
|
||||||
|
gsettings:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: gsettings
|
||||||
|
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.8"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -115,6 +424,94 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.8.0"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+1"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.7"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.9.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -171,6 +568,38 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
mobile_number:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: mobile_number
|
||||||
|
sha256: "8dc5b7a19193f7a41fa07aa933c8db8085033e6201b20323f20e4d283ea2a9a8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
package_info_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus
|
||||||
|
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.0.1"
|
||||||
|
package_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_platform_interface
|
||||||
|
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -227,6 +656,62 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.3"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.0.1"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: e20daf680eef1ca62ffe8c8c526b778cc386d50137c77ac71c8ec9c88c13fb9d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.9"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3+5"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -243,6 +728,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -280,6 +773,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -304,6 +805,78 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.20"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.4"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.3"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.5"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.3"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -312,6 +885,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
vibration:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: vibration
|
||||||
|
sha256: "9bb06614c69260f8bd11c80fe01ed7988905cf00e3417d656c2647e41f261d87"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.8"
|
||||||
|
vibration_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vibration_platform_interface
|
||||||
|
sha256: "258c273268f8aa40c88d29741137c536874a738779b92ddb8aa32ed093721ec5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.2"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -328,6 +917,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.15.0"
|
||||||
|
win32_registry:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32_registry
|
||||||
|
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -336,6 +941,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
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:
|
sdks:
|
||||||
dart: ">=3.7.0 <4.0.0"
|
dart: ">=3.8.0 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.32.0"
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,33 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
cupertino_icons: ^1.0.5
|
cupertino_icons: ^1.0.5
|
||||||
google_fonts: ^5.1.0
|
google_fonts: ^5.1.0
|
||||||
|
image_picker: ^1.2.1
|
||||||
|
permission_handler: ^12.0.3
|
||||||
|
flutter_contacts: ^1.1.9
|
||||||
|
geolocator: ^14.0.2
|
||||||
|
geocoding: ^4.0.0
|
||||||
|
url_launcher: ^6.3.2
|
||||||
|
mobile_number: ^3.0.0
|
||||||
|
vibration: ^3.1.8
|
||||||
|
audioplayers: ^5.2.1
|
||||||
|
flutter_haptic_feedback: ^1.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^3.0.0
|
flutter_lints: ^3.0.0
|
||||||
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: "launcher_icon"
|
||||||
|
ios: true
|
||||||
|
image_path: "assets/logo/pozo_logo_padded.png"
|
||||||
|
remove_alpha_ios: true
|
||||||
|
adaptive_icon_background: "#d2242e"
|
||||||
|
adaptive_icon_foreground: "assets/logo/pozo_logo_foreground.png"
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- assets/logo/
|
||||||
|
- assets/sounds/
|
||||||
|
|
|
||||||