Initial commit with clean Flutter & React codebase

This commit is contained in:
Sridhar R 2026-06-10 13:29:04 +05:30
commit f5a2e02839
104 changed files with 12934 additions and 0 deletions

9
.env.example Normal file
View File

@ -0,0 +1,9 @@
# GEMINI_API_KEY: Required for Gemini AI API calls.
# AI Studio automatically injects this at runtime from user secrets.
# Users configure this via the Secrets panel in the AI Studio UI.
GEMINI_API_KEY="MY_GEMINI_API_KEY"
# APP_URL: The URL where this applet is hosted.
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL"

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example
.antigravity/
*.zip

11
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "npm install && npm run dev",
"type": "node-terminal",
"request": "launch",
"command": "npm install && npm run dev"
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"workbench.startupEditor": "readme"
}

19
README.md Normal file
View File

@ -0,0 +1,19 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
https://ai.studio/apps/35aee5a3-67fc-4ea9-87e1-f8abb0400a3b
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

45
flutter_app/.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
flutter_app/.metadata Normal file
View File

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "8defaa71a77c16e8547abdbfad2053ce3a6e2d5b"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b
base_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b
- platform: ios
create_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b
base_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
flutter_app/README.md Normal file
View File

@ -0,0 +1,16 @@
# pozo_app
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@ -0,0 +1,34 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
analyzer:
errors:
deprecated_member_use: ignore
prefer_const_constructors: ignore
prefer_const_literals_to_create_immutables: ignore
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
flutter_app/android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.pozo_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.pozo_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="pozo_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.example.pozo_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=false

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

View File

@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

34
flutter_app/ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.pozoApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.pozoApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.pozoApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.pozoApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.pozoApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.pozoApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Pozo App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>pozo_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

22
flutter_app/lib/main.dart Normal file
View File

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'theme.dart';
import 'screens/welcome_screen.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const PozoApp());
}
class PozoApp extends StatelessWidget {
const PozoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PozoApp',
theme: PozoTheme.lightTheme,
debugShowCheckedModeBanner: false,
home: const WelcomeScreen(),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,820 @@
import 'package:flutter/material.dart';
import 'dart:ui';
import '../theme.dart';
import 'customize_setup_screen.dart';
class BusinessSetupScreen extends StatefulWidget {
const BusinessSetupScreen({super.key});
@override
State<BusinessSetupScreen> createState() => _BusinessSetupScreenState();
}
class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _gstinController = TextEditingController();
String _selectedCategory = 'supermarket';
bool _isNameValid = false;
final Set<String> _selectedDocs = {'GSTIN', 'FSSAI'};
final Map<String, String> _uploadedDocs = {};
@override
void initState() {
super.initState();
_nameController.addListener(() {
setState(() {
_isNameValid = _nameController.text.trim().isNotEmpty;
});
});
}
@override
void dispose() {
_nameController.dispose();
_gstinController.dispose();
super.dispose();
}
final List<Map<String, dynamic>> _categories = [
{'id': 'supermarket', 'name': 'Supermarket', 'icon': Icons.shopping_cart},
{'id': 'restaurant', 'name': 'Restaurant', 'icon': Icons.restaurant},
{'id': 'clothing', 'name': 'Clothing', 'icon': Icons.checkroom},
{'id': 'kirana', 'name': 'Kirana', 'icon': Icons.local_mall},
{'id': 'grocers', 'name': 'Grocers', 'icon': Icons.storefront},
{'id': 'others', 'name': 'Others', 'icon': Icons.grid_view},
];
Widget _buildDocChip(String label, IconData icon) {
final theme = Theme.of(context);
final isSelected = _selectedDocs.contains(label);
return InkWell(
onTap: () {
setState(() {
if (isSelected) {
_selectedDocs.remove(label);
} else {
_selectedDocs.add(label);
}
});
},
borderRadius: BorderRadius.circular(8),
child: Container(
constraints: const BoxConstraints(minHeight: 48),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFFAF0F0) : Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? const Color(0xFFAF101A) : theme.colorScheme.outlineVariant,
width: isSelected ? 1.5 : 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[600],
),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[800],
),
),
],
),
),
);
}
Widget _buildUploadSection(String docName, IconData icon) {
final theme = Theme.of(context);
final isUploaded = _uploadedDocs.containsKey(docName);
return Container(
margin: const EdgeInsets.only(top: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: theme.colorScheme.outlineVariant,
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(icon, color: const Color(0xFFAF101A), size: 18),
const SizedBox(width: 8),
Text(
'$docName Document',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
],
),
IconButton(
icon: const Icon(Icons.close, size: 18, color: Colors.grey),
onPressed: () {
setState(() {
_selectedDocs.remove(docName);
});
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
)
],
),
const SizedBox(height: 12),
if (isUploaded) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Icon(
Icons.insert_drive_file,
color: Color(0xFFAF101A),
size: 24,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_uploadedDocs[docName] ?? 'Attached_document.pdf',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 2),
const Text(
'Size: 1.2 MB • Ready',
style: TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () {
setState(() {
_uploadedDocs.remove(docName);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$docName attachment removed.'),
behavior: SnackBarBehavior.floating,
),
);
},
),
],
),
),
] else ...[
CustomPaint(
painter: DashedBorderPainter(
color: const Color(0xFFAF101A),
radius: 16,
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
const Icon(
Icons.cloud_upload_outlined,
color: Color(0xFFAF101A),
size: 32,
),
const SizedBox(height: 8),
Text(
'Upload your $docName Proof',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
setState(() {
_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),
label: const Text('Take Photo'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 36),
backgroundColor: const Color(0xFFAF101A),
foregroundColor: Colors.white,
elevation: 1,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: () {
setState(() {
_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),
label: const Text('From Gallery'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFAF101A),
side: const BorderSide(color: Color(0xFFAF101A)),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
],
),
),
),
],
],
),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final padding = context.responsivePadding;
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
onPressed: () => Navigator.pop(context),
),
title: Text(
'PozoApp',
style: theme.textTheme.headlineMedium?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
actions: [
Padding(
padding: EdgeInsets.symmetric(horizontal: padding),
child: Center(
child: Text(
'Step 3 of 5',
style: theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
)
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(6),
child: Row(
children: List.generate(5, (index) {
return Expanded(
child: Container(
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 2.0),
decoration: BoxDecoration(
color: index <= 2 ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(3),
),
),
);
}),
),
),
),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Complete verification',
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 6),
Text(
'Verify your business details to start selling.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
// Name Field Input
Text(
'Business/Shop Name *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextField(
controller: _nameController,
decoration: const InputDecoration(
hintText: 'e.g. Sharma General Store',
),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Category selection Block
Text(
'Business Type *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
// Category custom grid (2x3)
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 2.2,
),
itemCount: _categories.length,
itemBuilder: (context, index) {
final cat = _categories[index];
final isSelected = _selectedCategory == cat['id'];
return InkWell(
onTap: () {
setState(() {
_selectedCategory = cat['id'];
});
},
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
color: isSelected
? const Color(0xFFAF101A).withOpacity(0.08)
: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? const Color(0xFFAF101A) : const Color(0xFFE2E2E2),
width: isSelected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.01),
blurRadius: 4,
offset: const Offset(0, 2),
)
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: Icon(
cat['icon'],
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[600],
size: 26,
),
),
const SizedBox(height: 6),
Center(
child: Text(
cat['name'],
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isSelected ? const Color(0xFFAF101A) : Colors.grey[800],
),
),
)
],
),
),
);
},
),
const SizedBox(height: 16),
// GSTIN optional Input group with Verify button on right-side
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'GSTIN',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
Text(
'(Optional)',
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey[500],
),
)
],
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextField(
controller: _gstinController,
textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration(
hintText: '15-DIGIT GST NUMBER',
),
style: const TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('GSTIN Verified Successfully!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
},
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 48),
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text('Verify'),
),
],
),
const SizedBox(height: 6),
Text(
"Auto-fetches address & legal name on verification.",
style: TextStyle(fontSize: 11, color: Colors.grey[500]),
),
const SizedBox(height: 16),
// Shop Location Detect
Text(
'Shop Location *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('📍 Location detected: Sector 62, Noida, UP'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
},
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: theme.colorScheme.outlineVariant,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.my_location,
color: theme.colorScheme.primary,
size: 20,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Detect current location',
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
const SizedBox(height: 2),
Text(
'Pin shop address automatically',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
],
),
),
Icon(
Icons.chevron_right,
color: Colors.grey[400],
),
],
),
),
),
const SizedBox(height: 16),
// Identity/License Documents
Text(
'Identity / License Documents (Select all that apply)',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildDocChip('GSTIN', Icons.description),
_buildDocChip('FSSAI', Icons.restaurant),
_buildDocChip('Aadhar', Icons.badge_outlined),
_buildDocChip('Udhyog Aadhar', Icons.domain),
_buildDocChip('PAN', Icons.credit_card),
],
),
const SizedBox(height: 8),
// Expanded Upload Sections
..._selectedDocs.map((doc) {
IconData docIcon = Icons.description;
if (doc == 'GSTIN') docIcon = Icons.description;
if (doc == 'FSSAI') docIcon = Icons.restaurant;
if (doc == 'Aadhar') docIcon = Icons.badge_outlined;
if (doc == 'Udhyog Aadhar') docIcon = Icons.domain;
if (doc == 'PAN') docIcon = Icons.credit_card;
return _buildUploadSection(doc, docIcon);
}),
const SizedBox(height: 16),
// Shop Facade Photo upload area
Text(
'Shop Facade Photo *',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('📸 Camera / Photo Upload Clicked!'),
behavior: SnackBarBehavior.floating,
),
);
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: theme.colorScheme.outlineVariant,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.add_a_photo_outlined,
color: theme.colorScheme.primary,
size: 24,
),
),
const SizedBox(height: 12),
Text(
'Upload front view',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
'Clear photo showing shop board',
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
),
),
],
),
),
),
const SizedBox(height: 12),
],
),
),
),
),
// Button docked
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: ElevatedButton(
onPressed: _isNameValid
? () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CustomizeSetupScreen(
businessName: _nameController.text.trim(),
businessType: _selectedCategory,
gstin: _gstinController.text.trim(),
),
),
);
}
: null,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(54),
backgroundColor: _isNameValid ? theme.colorScheme.primary : Colors.grey[200],
foregroundColor: _isNameValid ? Colors.white : Colors.grey[400],
elevation: _isNameValid ? 2 : 0,
),
child: const Text('Continue to Verification'),
),
)
],
),
),
),
),
);
}
}
class DashedBorderPainter extends CustomPainter {
final Color color;
final double strokeWidth;
final double gap;
final double radius;
DashedBorderPainter({
required this.color,
this.strokeWidth = 1.5,
this.gap = 4.0,
this.radius = 16.0,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
final RRect rrect = RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height),
Radius.circular(radius),
);
try {
final Path path = Path()..addRRect(rrect);
final Path dashPath = Path();
double distance = 0.0;
bool draw = true;
for (final PathMetric metric in path.computeMetrics()) {
while (distance < metric.length) {
final double len = draw ? gap * 1.5 : gap;
if (draw) {
dashPath.addPath(
metric.extractPath(distance, (distance + len).clamp(0.0, metric.length)),
Offset.zero,
);
}
distance += len;
draw = !draw;
}
}
canvas.drawPath(dashPath, paint);
} catch (e) {
// Fallback for platform/renderers (such as Flutter Web HTML renderer) that do not support computeMetrics()
canvas.drawRRect(rrect, paint);
}
}
@override
bool shouldRepaint(covariant DashedBorderPainter oldDelegate) {
return oldDelegate.color != color ||
oldDelegate.strokeWidth != strokeWidth ||
oldDelegate.gap != gap ||
oldDelegate.radius != radius;
}
}

View File

@ -0,0 +1,273 @@
import 'package:flutter/material.dart';
import '../theme.dart';
import 'setup_complete_screen.dart';
class CustomizeSetupScreen extends StatefulWidget {
final String businessName;
final String businessType;
final String gstin;
const CustomizeSetupScreen({
super.key,
required this.businessName,
required this.businessType,
this.gstin = '',
});
@override
State<CustomizeSetupScreen> createState() => _CustomizeSetupScreenState();
}
class _CustomizeSetupScreenState extends State<CustomizeSetupScreen> {
bool _barcode = false;
bool _kot = false;
bool _inventory = true;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final padding = context.responsivePadding;
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
onPressed: () => Navigator.pop(context),
),
title: Row(
children: const [
Icon(Icons.settings, color: Color(0xFFBA1A20), size: 18),
SizedBox(width: 6),
Text(
'PozoApp',
style: TextStyle(
color: Color(0xFFBA1A20),
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SetupCompleteScreen(
businessName: widget.businessName,
businessType: widget.businessType,
gstin: widget.gstin,
barcodeScanner: false,
kotManagement: false,
inventoryAlerts: true,
),
),
);
},
child: Text(
'Skip for Now',
style: TextStyle(
color: Colors.grey[750],
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
)
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(6),
child: Row(
children: List.generate(5, (index) {
return Expanded(
child: Container(
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 2.0),
decoration: BoxDecoration(
color: index <= 3 ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(3),
),
),
);
}),
),
),
),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10),
color: Colors.grey[50],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text('Step 4 of 5', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.grey)),
Text('Smart Setup', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFFBA1A20))),
],
),
),
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Customize Your Setup',
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Turn on the features you need for your shop. You can always change these later in settings.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
// Toggle 1: Barcode
_buildToggleCard(
Icons.qr_code_scanner,
'Barcode Scanner',
'Do you use a Barcode Scanner?',
_barcode,
(val) => setState(() => _barcode = val),
theme,
),
const SizedBox(height: 12),
// Toggle 2: KOT
_buildToggleCard(
Icons.receipt_long,
'KOT Management',
'Do you need Kitchen Order Ticket management?',
_kot,
(val) => setState(() => _kot = val),
theme,
),
const SizedBox(height: 12),
// Toggle 3: Inventory stock tracking
_buildToggleCard(
Icons.inventory_2,
'Inventory Alerts',
'Get notified when stock is running low.',
_inventory,
(val) => setState(() => _inventory = val),
theme,
),
],
),
),
),
),
// Pinned action keys
Padding(
padding: EdgeInsets.all(padding),
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SetupCompleteScreen(
businessName: widget.businessName,
businessType: widget.businessType,
gstin: widget.gstin,
barcodeScanner: _barcode,
kotManagement: _kot,
inventoryAlerts: _inventory,
),
),
);
},
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(54),
),
child: const Text('Save & Continue'),
),
)
],
),
),
),
),
);
}
Widget _buildToggleCard(
IconData icon,
String title,
String subtitle,
bool value,
ValueChanged<bool> onChanged,
ThemeData theme,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey[200]!),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.grey[600], size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
),
)
],
),
),
Switch(
value: value,
onChanged: onChanged,
activeColor: Colors.white,
activeTrackColor: theme.colorScheme.primary,
),
],
),
);
}
}

View File

@ -0,0 +1,474 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme.dart';
import 'otp_verification_screen.dart';
class MobileLoginScreen extends StatefulWidget {
const MobileLoginScreen({super.key});
@override
State<MobileLoginScreen> createState() => _MobileLoginScreenState();
}
class _MobileLoginScreenState extends State<MobileLoginScreen> {
final TextEditingController _controller = TextEditingController();
bool _isValid = false;
@override
void initState() {
super.initState();
_controller.addListener(_validateInput);
}
void _validateInput() {
setState(() {
_isValid = _controller.text.length == 10;
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final gap = context.responsiveGap;
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFFAF101A)),
onPressed: () => Navigator.pop(context),
),
title: Text(
'PozoApp',
style: theme.textTheme.headlineMedium?.copyWith(
color: const Color(0xFFAF101A),
fontWeight: FontWeight.bold,
),
),
centerTitle: false,
elevation: 0,
backgroundColor: Colors.transparent,
),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Progress Bar: Step 2 of 5 with "Mobile Login" label on right
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Step 2 of 5',
style: theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurfaceVariant,
),
),
const Text(
'Mobile Login',
style: TextStyle(
fontFamily: 'Plus Jakarta Sans',
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFAF101A),
),
),
],
),
const SizedBox(height: 8),
Row(
children: List.generate(5, (index) {
return Expanded(
child: Container(
height: 6,
margin: EdgeInsets.only(
left: index == 0 ? 0 : 3.0,
right: index == 4 ? 0 : 3.0,
),
decoration: BoxDecoration(
color: index <= 1 ? const Color(0xFFAF101A) : const Color(0xFFE2E2E2),
borderRadius: BorderRadius.circular(100),
),
),
);
}),
),
const SizedBox(height: 16),
// Title Header & Secure Badge Group
Text(
'Welcome Back',
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Enter your mobile number to sign in or create a new account.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
// 6. Top Badge: Green "Secure OTP Verification" chip with lock icon
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFFAFFD1).withOpacity(0.8),
borderRadius: BorderRadius.circular(100),
border: Border.all(color: const Color(0xFF016619).withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.lock, size: 14, color: Color(0xFF016619)),
const SizedBox(width: 6),
Text(
'Secure OTP Verification',
style: theme.textTheme.labelSmall?.copyWith(
color: const Color(0xFF016619),
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 16),
// 2. Truecaller Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFF3F3F3), // light gray background (surface low)
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE4BEBA).withOpacity(0.5)),
),
child: Column(
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Color(0xFF005FAF),
shape: BoxShape.circle,
),
child: const Icon(
Icons.verified_user, // blue shield icon
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Verify instantly with',
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
const Text(
'Truecaller',
style: TextStyle(
fontFamily: 'Plus Jakarta Sans',
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
],
),
),
],
),
const SizedBox(height: 20),
const Text(
'+91 98765 43210',
style: TextStyle(
fontFamily: 'Plus Jakarta Sans',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
_controller.text = '9876543210';
setState(() {
_isValid = true;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('⚡ Phone verified automatically with Truecaller! Joining PozoApp...'),
backgroundColor: Color(0xFF005FAF),
behavior: SnackBarBehavior.floating,
),
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const OtpVerificationScreen(
mobileNumber: '9876543210',
),
),
);
},
icon: const Icon(Icons.flash_on, size: 18, color: Colors.white),
label: const Text(
'1-Tap Verification',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF005FAF),
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // 8px for buttons
),
elevation: 1,
),
),
const SizedBox(height: 12),
Text(
'No OTP required. Your phone will be verified automatically.',
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
height: 1.4,
),
),
],
),
),
SizedBox(height: gap),
// 3. Divider: "Or enter mobile number manually"
Row(
children: [
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Or enter mobile number manually',
style: theme.textTheme.labelMedium?.copyWith(
color: Colors.grey[600],
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
),
const Expanded(child: Divider(color: Color(0xFFE4BEBA))),
],
),
SizedBox(height: gap),
Text(
'Mobile Number',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
// 4. Mobile Number field: Country code group on left, number field on right
Container(
height: 56,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.colorScheme.outlineVariant),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 4,
offset: const Offset(0, 2),
)
],
),
child: Row(
children: [
// Flag and code prefix box
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border(
right: BorderSide(color: theme.colorScheme.outlineVariant),
),
),
child: Row(
children: [
// Indian flag representation
Container(
width: 22,
height: 14,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!, width: 0.5),
),
child: Column(
children: [
Container(height: 4, color: const Color(0xFFFF9933)),
Container(
height: 4,
color: Colors.white,
child: Center(
child: Container(
width: 2,
height: 2,
decoration: const BoxDecoration(
color: Color(0xFF000080),
shape: BoxShape.circle,
),
),
),
),
Container(height: 4, color: const Color(0xFF128807)),
],
),
),
const SizedBox(width: 8),
Text(
'+91',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
)
],
),
),
// Input field
Expanded(
child: TextField(
controller: _controller,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
decoration: const InputDecoration(
hintText: '00000 00000',
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
filled: false,
contentPadding: EdgeInsets.symmetric(horizontal: 16),
),
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
)
],
),
),
SizedBox(height: gap * 2),
],
),
),
),
// 5. Bottom buttons
Column(
mainAxisSize: MainAxisSize.min,
children: [
// Full width red "Send OTP" button
ElevatedButton(
onPressed: _isValid
? () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OtpVerificationScreen(
mobileNumber: _controller.text,
),
),
);
}
: null,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(56),
backgroundColor: _isValid ? const Color(0xFFAF101A) : Colors.grey[200],
foregroundColor: _isValid ? Colors.white : Colors.grey[400],
elevation: _isValid ? 2 : 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // 8px default button radius
),
),
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,
),
);
},
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(56),
side: const BorderSide(color: Color(0xFFAF101A), width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // 8px default button radius
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.forum, color: Color(0xFFAF101A), size: 18),
const SizedBox(width: 8),
const Text(
'Help / Talk to Support',
style: TextStyle(
color: Color(0xFFAF101A),
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
],
),
],
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,382 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../theme.dart';
import 'business_setup_screen.dart';
class OtpVerificationScreen extends StatefulWidget {
final String mobileNumber;
const OtpVerificationScreen({super.key, required this.mobileNumber});
@override
State<OtpVerificationScreen> createState() => _OtpVerificationScreenState();
}
class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
late Timer _timer;
int _timeLeft = 45;
final List<TextEditingController> _controllers = List.generate(6, (_) => TextEditingController());
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
@override
void initState() {
super.initState();
_startCountdown();
}
void _startCountdown() {
_timeLeft = 45;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_timeLeft <= 0) {
_timer.cancel();
} else {
setState(() {
_timeLeft--;
});
}
});
}
@override
void dispose() {
_timer.cancel();
for (var controller in _controllers) {
controller.dispose();
}
for (var node in _focusNodes) {
node.dispose();
}
super.dispose();
}
String _getFormattedTime() {
return '00:${_timeLeft.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final padding = context.responsivePadding;
final displayMobile = widget.mobileNumber.length == 10
? '+91 ${widget.mobileNumber.substring(0, 5)} ${widget.mobileNumber.substring(5)}'
: '+91 ${widget.mobileNumber}';
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFFBA1A20)),
onPressed: () => Navigator.pop(context),
),
title: Text(
'PozoApp',
style: theme.textTheme.headlineMedium?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
actions: [
Padding(
padding: EdgeInsets.symmetric(horizontal: padding),
child: Center(
child: Text(
'Step 2.5 of 5',
style: theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
)
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(6),
child: Row(
children: List.generate(5, (index) {
return Expanded(
child: Container(
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 2.0),
decoration: BoxDecoration(
color: index <= 1
? theme.colorScheme.primary
: (index == 2 ? theme.colorScheme.primary.withOpacity(0.5) : theme.colorScheme.surfaceContainerHighest),
borderRadius: BorderRadius.circular(3),
),
),
);
}),
),
),
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Alert envelope pulse symbol
Center(
child: Container(
width: 96,
height: 96,
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh,
shape: BoxShape.circle,
),
child: Center(
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 10,
)
],
),
child: Icon(
Icons.sms,
color: theme.colorScheme.primary,
size: 38,
),
),
),
),
),
const SizedBox(height: 16),
Text(
'Verifying your number',
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Sent code to $displayMobile",
style: theme.textTheme.bodyMedium,
),
const SizedBox(width: 8),
GestureDetector(
onTap: () => Navigator.pop(context),
child: Text(
'Edit',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
],
),
const SizedBox(height: 16),
// 6 Custom Inputs Fields
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(11, (index) {
if (index.isOdd) {
return const SizedBox(width: 8);
}
final fieldIndex = index ~/ 2;
return SizedBox(
width: 44,
child: TextField(
controller: _controllers[fieldIndex],
focusNode: _focusNodes[fieldIndex],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
decoration: InputDecoration(
counterText: '',
contentPadding: const EdgeInsets.symmetric(vertical: 12),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: theme.colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: theme.colorScheme.primary, width: 2),
),
),
onChanged: (value) {
if (value.isNotEmpty && fieldIndex < 5) {
_focusNodes[fieldIndex + 1].requestFocus();
}
},
),
);
}),
),
const SizedBox(height: 16),
// Auto detect loader
Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(theme.colorScheme.primary),
),
),
const SizedBox(width: 12),
Text(
'Auto-detecting OTP...',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 16),
// Resending timer action column
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.schedule, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Text(
_timeLeft > 0
? 'Resend code in ${_getFormattedTime()}'
: 'Resend Code via SMS',
style: theme.textTheme.bodyMedium,
),
],
),
const SizedBox(height: 16),
// WhatsApp custom CTA button
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
side: const BorderSide(color: Color(0xFFE2E2E2)),
elevation: 1,
minimumSize: const Size(200, 48),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Text(
'Resend via WhatsApp',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
],
),
)
],
),
),
),
),
),
),
// Pinned Bottom Dock
Container(
height: 80,
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey[200]!)),
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFooterAction(Icons.help_outline, 'Help'),
_buildFooterAction(Icons.save, 'Save Draft'),
TextButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BusinessSetupScreen(),
),
);
},
icon: const Text(
'Verify Manually',
style: TextStyle(fontWeight: FontWeight.bold),
),
label: const Icon(Icons.arrow_forward, size: 16),
style: TextButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: theme.colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
)
],
),
),
),
),
)
],
),
),
);
}
Widget _buildFooterAction(IconData icon, String label) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: Colors.grey[600], size: 22),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
)
],
);
}
}

View File

@ -0,0 +1,342 @@
import 'package:flutter/material.dart';
import '../theme.dart';
import 'billing_dashboard_screen.dart';
class SetupCompleteScreen extends StatelessWidget {
final String businessName;
final String businessType;
final String gstin;
final bool barcodeScanner;
final bool kotManagement;
final bool inventoryAlerts;
const SetupCompleteScreen({
super.key,
required this.businessName,
required this.businessType,
this.gstin = '',
this.barcodeScanner = false,
this.kotManagement = false,
this.inventoryAlerts = true,
});
String _getCategoryDisplayName(String id) {
final cleanId = id.replaceAll('category/', '').trim().toLowerCase();
switch (cleanId) {
case 'supermarket':
return 'Supermarket';
case 'restaurant':
return 'Restaurant';
case 'clothing':
return 'Clothing / Boutique';
case 'kirana':
return 'Retail / Grocery (Kirana)';
case 'others':
default:
return 'Retail / Others';
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final categoryText = _getCategoryDisplayName(businessType);
final gap = context.responsiveGap;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Row(
children: const [
Icon(Icons.check_circle, color: Colors.green, size: 18),
SizedBox(width: 6),
Text(
'Step 5 of 5 Completed',
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(6),
child: Container(
height: 6,
color: theme.colorScheme.primary,
),
),
),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 24),
// Circular Green Check badge
Container(
width: 96,
height: 96,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 16,
offset: Offset(0, 8),
)
],
),
child: Center(
child: Container(
width: 72,
height: 72,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
color: Colors.white,
size: 40,
),
),
),
),
SizedBox(height: gap),
Text(
'Setup Complete! 🎉',
textAlign: TextAlign.center,
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Your billing dashboard is ready for action.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium,
),
SizedBox(height: gap * 1.5),
// Glassmorphism summary Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.grey[200]!),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 20,
offset: const Offset(0, 8),
)
],
),
child: Column(
children: [
// Row 1: Store name
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: const Icon(Icons.storefront, color: Colors.black87),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'BUSINESS NAME',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.grey,
letterSpacing: 1.0,
),
),
const SizedBox(height: 4),
Text(
businessName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
)
],
),
// Divider line
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Divider(color: Colors.grey[200]),
),
// Row 2: Category type
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: const Icon(Icons.category, color: Colors.black87),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'BUSINESS TYPE',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.grey,
letterSpacing: 1.0,
),
),
const SizedBox(height: 4),
Text(
categoryText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
)
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Divider(color: Colors.grey[200]),
),
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: const Icon(Icons.settings, color: Colors.black87),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'ACTIVE CONFIGURATIONS',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.grey,
letterSpacing: 1.0,
),
),
const SizedBox(height: 4),
Text(
[
if (barcodeScanner) 'Barcode Enabled',
if (kotManagement) 'KOT/Kitchen Order',
if (inventoryAlerts) 'Low Stock Warnings',
if (!barcodeScanner && !kotManagement && !inventoryAlerts) 'Standard Register',
].join(', '),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
)
],
)
],
),
),
const SizedBox(height: 24),
// Primary Launcher CTA
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('⚡ Launching Register Dashboard... Enjoy billing!'),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.green,
duration: Duration(milliseconds: 1500),
),
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BillingDashboardScreen(
businessName: businessName,
businessType: businessType,
gstin: gstin,
barcodeScanner: barcodeScanner,
kotManagement: kotManagement,
inventoryAlerts: inventoryAlerts,
),
),
);
},
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.receipt, size: 20),
SizedBox(width: 8),
Text('Create Your First Bill'),
],
),
),
const SizedBox(height: 24),
],
),
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,494 @@
import 'package:flutter/material.dart';
import '../theme.dart';
import 'mobile_login_screen.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isMobile = context.isMobile;
return Scaffold(
body: SafeArea(
child: Column(
children: [
// Top Progress bar indicator - 1st segment active
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: Row(
children: List.generate(5, (index) {
return Expanded(
child: Container(
height: 4,
margin: const EdgeInsets.symmetric(horizontal: 2.0),
decoration: BoxDecoration(
color: index == 0 ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(2),
),
),
);
}),
),
),
// Scrollable / Responsive Content
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
if (maxWidth >= 768) {
// Desktop / Tablet Double-Column Layout
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1000),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Left column: content + button
Expanded(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLogo(theme),
const SizedBox(height: 16),
Text(
'Manage your business billing in 3 clicks!',
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'The fastest, most reliable tool for Indian MSMEs to run their shop.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
_buildSeal(theme),
const SizedBox(height: 16),
_buildCTAColumn(context, theme),
],
),
),
const SizedBox(width: 32),
// Right column: mock card
Expanded(
flex: 4,
child: Center(
child: _buildMockDashboard(theme),
),
),
],
),
),
),
),
);
} else {
// Mobile Single Column Layout
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
_buildLogo(theme),
const SizedBox(height: 16),
Text(
'Manage your business billing in 3 clicks!',
textAlign: TextAlign.left,
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'The fastest, most reliable tool for Indian MSMEs to run their shop.',
textAlign: TextAlign.left,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
_buildSeal(theme),
const SizedBox(height: 24),
Center(child: _buildMockDashboard(theme)),
const SizedBox(height: 24),
],
),
),
);
}
},
),
),
// Pinned Bottom Button on Mobile
if (isMobile)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: _buildCTAColumn(context, theme),
),
],
),
),
);
}
Widget _buildLogo(ThemeData theme) {
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withOpacity(0.15),
blurRadius: 24,
offset: const Offset(0, 8),
)
],
),
alignment: Alignment.center,
child: Stack(
clipBehavior: Clip.none,
children: [
Text(
'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,
),
),
),
)
],
),
);
}
Widget _buildSeal(ThemeData theme) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: theme.colorScheme.outlineVariant),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.verified,
size: 16,
color: theme.colorScheme.tertiary,
),
const SizedBox(width: 6),
Text(
'Trusted by 10,000+ merchants',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
);
}
Widget _buildMockDashboard(ThemeData theme) {
return Container(
width: 280,
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,
),
),
),
)
],
),
);
}
Widget _buildCTAColumn(BuildContext context, ThemeData theme) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: context.isMobile ? CrossAxisAlignment.center : CrossAxisAlignment.start,
children: [
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MobileLoginScreen()),
);
},
style: ElevatedButton.styleFrom(
minimumSize: const Size(260, 54),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: const [
Text('Get Started'),
SizedBox(width: 8),
Icon(Icons.arrow_forward, size: 18),
],
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: context.isMobile ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Colors.grey[300],
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Colors.grey[300],
shape: BoxShape.circle,
),
),
],
)
],
);
}
Widget _buildMockRow(IconData icon, Color bg, Color color) {
return Container(
height: 48,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: bg.withOpacity(0.4),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[100]!),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 16),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 6,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(3),
),
),
const SizedBox(height: 6),
Container(
width: 50,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
],
),
),
Icon(Icons.more_vert, color: Colors.grey[400], size: 16)
],
),
);
}
}

175
flutter_app/lib/theme.dart Normal file
View File

@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class PozoTheme {
// Brand color palette definitions
static const Color primary = Color(0xFFBA1A20);
static const Color primaryContainer = Color(0xFFD32F2F);
static const Color onPrimary = Color(0xFFFFFFFF);
static const Color background = Color(0xFFF9F9F9);
static const Color surface = Color(0xFFF9F9F9);
static const Color surfaceContainerLowest = Color(0xFFFFFFFF);
static const Color surfaceContainerLow = Color(0xFFF3F3F3);
static const Color surfaceContainerHigh = Color(0xFFE8E8E8);
static const Color onSurface = Color(0xFF1A1C1C);
static const Color onSurfaceVariant = Color(0xFF5B403D);
static const Color outline = Color(0xFF8F6F6C);
static const Color outlineVariant = Color(0xFFE4BEBA);
static const Color tertiary = Color(0xFF016619);
static const Color tertiaryFixed = Color(0xFF9DF898);
static const Color error = Color(0xFFBA1A1A);
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
colorScheme: const ColorScheme.light(
primary: primary,
primaryContainer: primaryContainer,
onPrimary: onPrimary,
surface: surface,
onSurface: onSurface,
onSurfaceVariant: onSurfaceVariant,
outline: outline,
outlineVariant: outlineVariant,
tertiary: tertiary,
error: error,
),
scaffoldBackgroundColor: background,
textTheme: TextTheme(
// Plus Jakarta Sans headings
displayLarge: GoogleFonts.plusJakartaSans(
fontSize: 30,
fontWeight: FontWeight.bold,
height: 1.25,
color: onSurface,
),
headlineLarge: GoogleFonts.plusJakartaSans(
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.33,
color: onSurface,
),
headlineMedium: GoogleFonts.plusJakartaSans(
fontSize: 22,
fontWeight: FontWeight.w600,
height: 1.27,
color: onSurface,
),
// Inter helper body fonts
bodyLarge: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.normal,
height: 1.44,
color: onSurfaceVariant,
),
bodyMedium: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.normal,
height: 1.5,
color: onSurfaceVariant,
),
labelLarge: GoogleFonts.inter(
fontSize: 20,
fontWeight: FontWeight.w600,
height: 1.2,
letterSpacing: 0.5,
),
labelMedium: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
letterSpacing: 1.0,
),
labelSmall: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.bold,
letterSpacing: 1.0,
),
),
// Inputs decorating styles
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: surfaceContainerLowest,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: primary, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: error),
),
hintStyle: GoogleFonts.inter(color: outlineVariant, fontSize: 16),
),
// Button themes helper
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primary,
foregroundColor: onPrimary,
minimumSize: const Size.fromHeight(54),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
textStyle: GoogleFonts.plusJakartaSans(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
}
}
extension ResponsiveLayout on BuildContext {
double get responsivePadding {
final width = MediaQuery.of(this).size.width;
if (width < 600) {
return 16.0;
} else if (width < 1024) {
return 24.0;
} else {
return 32.0;
}
}
double get responsiveMargin {
final width = MediaQuery.of(this).size.width;
if (width < 600) {
return 12.0;
} else if (width < 1024) {
return 20.0;
} else {
return 28.0;
}
}
double get responsiveGap {
final width = MediaQuery.of(this).size.width;
if (width < 600) {
return 16.0;
} else if (width < 1024) {
return 24.0;
} else {
return 32.0;
}
}
bool get isMobile => MediaQuery.of(this).size.width < 600;
bool get isTablet => MediaQuery.of(this).size.width >= 600 && MediaQuery.of(this).size.width < 1024;
bool get isWeb => MediaQuery.of(this).size.width >= 1024;
}

341
flutter_app/pubspec.lock Normal file
View File

@ -0,0 +1,341 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: e20ff62b158b96f392bfc8afe29dee1503c94fbea2cbe8186fd59b756b8ae982
url: "https://pub.dev"
source: hosted
version: "5.1.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev"
source: hosted
version: "10.0.9"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37"
url: "https://pub.dev"
source: hosted
version: "2.2.19"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.29.0"

20
flutter_app/pubspec.yaml Normal file
View File

@ -0,0 +1,20 @@
name: pozo_app
description: "A premium billing onboarding and setup flow for Indian MSMEs built with Flutter."
version: 1.0.0+1
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.5
google_fonts: ^5.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true

View File

@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pozo_app/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const PozoApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

BIN
flutter_app/web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="pozo_app">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>pozo_app</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

View File

@ -0,0 +1,35 @@
{
"name": "pozo_app",
"short_name": "pozo_app",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1
metadata.json Normal file
View File

@ -0,0 +1 @@
{"name":"Pozo Modern Billing","description":"A premium, high-fidelity interactive billing onboarding and setup flow designed for Indian MSMEs.","requestFramePermissions":[],"majorCapabilities":["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]}

4284
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=3000 --host=0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist server.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^2.4.0",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"lucide-react": "^0.546.0",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"vite": "^6.2.3",
"express": "^4.21.2",
"dotenv": "^17.2.3",
"motion": "^12.23.24"
},
"devDependencies": {
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"esbuild": "^0.25.0",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.3",
"@types/express": "^4.17.21"
}
}

153
src/App.tsx Normal file
View File

@ -0,0 +1,153 @@
import React, { useState } from 'react';
import DeviceFrame from './components/DeviceFrame';
import WelcomeScreen from './components/WelcomeScreen';
import MobileLoginScreen from './components/MobileLoginScreen';
import OtpVerificationScreen from './components/OtpVerificationScreen';
import BusinessSetupScreen from './components/BusinessSetupScreen';
import CustomizeSetupScreen from './components/CustomizeSetupScreen';
import SetupCompleteScreen from './components/SetupCompleteScreen';
import BillingDashboard from './components/BillingDashboard';
import { OnboardingState, BusinessFeatures } from './types';
export default function App() {
// Initialize standard global onboarding states
const [state, setState] = useState<OnboardingState>({
step: 'welcome',
mobileNumber: '',
otpCode: ['', '', '', '', '', ''],
businessName: '',
businessType: 'supermarket',
gstin: '',
features: {
barcodeScanner: false,
kotManagement: false,
inventoryAlerts: true // Pre-checked by default as depicted in Screen 5
}
});
const handleReset = () => {
setState({
step: 'welcome',
mobileNumber: '',
otpCode: ['', '', '', '', '', ''],
businessName: '',
businessType: 'supermarket',
gstin: '',
features: {
barcodeScanner: false,
kotManagement: false,
inventoryAlerts: true
}
});
};
const handleWelcomeDone = () => {
setState(prev => ({ ...prev, step: 'mobile-login' }));
};
const handleMobileSubmitted = (mobile: string) => {
setState(prev => ({ ...prev, mobileNumber: mobile, step: 'otp-verification' }));
};
const handleOtpVerified = (otp: string[]) => {
setState(prev => ({ ...prev, otpCode: otp, step: 'business-setup' }));
};
const handleBusinessSubmitted = (data: { businessName: string; businessType: string; gstin: string }) => {
setState(prev => ({
...prev,
businessName: data.businessName,
businessType: data.businessType,
gstin: data.gstin,
step: 'customize-setup'
}));
};
const handleFeaturesCustomized = (features: BusinessFeatures) => {
setState(prev => ({ ...prev, features, step: 'setup-complete' }));
};
const handleBypassFeatures = () => {
setState(prev => ({ ...prev, step: 'setup-complete' }));
};
const handleOnboardingFinished = () => {
setState(prev => ({ ...prev, step: 'setup-complete' as any })); // Trigger direct ledger launch
// To route to Billing Register, we'll assign a simulated custom state 'billing-dashboard'
setState(prev => ({ ...prev, step: 'billing-dashboard' as any }));
};
// Switch router for the multi-stage form wizard
const renderStepContent = () => {
switch (state.step) {
case 'welcome':
return (
<WelcomeScreen
onContinue={handleWelcomeDone}
state={state}
/>
);
case 'mobile-login':
return (
<MobileLoginScreen
onBack={() => setState(prev => ({ ...prev, step: 'welcome' }))}
onContinue={handleMobileSubmitted}
state={state}
/>
);
case 'otp-verification':
return (
<OtpVerificationScreen
onBack={() => setState(prev => ({ ...prev, step: 'mobile-login' }))}
onContinue={handleOtpVerified}
state={state}
/>
);
case 'business-setup':
return (
<BusinessSetupScreen
onBack={() => setState(prev => ({ ...prev, step: 'otp-verification' }))}
onContinue={handleBusinessSubmitted}
state={state}
/>
);
case 'customize-setup':
return (
<CustomizeSetupScreen
onBack={() => setState(prev => ({ ...prev, step: 'business-setup' }))}
onSkip={handleBypassFeatures}
onContinue={handleFeaturesCustomized}
state={state}
/>
);
case 'setup-complete':
return (
<SetupCompleteScreen
onContinue={handleOnboardingFinished}
state={state}
/>
);
// Custom Post-onboarding functional simulator route
case 'billing-dashboard' as any:
return (
<BillingDashboard
state={state}
onReset={handleReset}
/>
);
default:
return (
<WelcomeScreen
onContinue={handleWelcomeDone}
state={state}
/>
);
}
};
return (
<DeviceFrame onReset={handleReset}>
{renderStepContent()}
</DeviceFrame>
);
}

View File

@ -0,0 +1,500 @@
import React, { useState } from 'react';
import { Plus, Minus, ShoppingCart, ArrowLeft, RefreshCw, Layers, Sparkles, Receipt, Camera, Printer, CheckCircle2, ShieldCheck } from 'lucide-react';
import { OnboardingState } from '../types';
interface BillingDashboardProps {
state: OnboardingState;
onReset: () => void;
}
interface ProductItem {
id: string;
name: string;
price: number;
stock: number;
code: string;
}
interface BillingItem {
product: ProductItem;
quantity: number;
}
const INVENTORY_PRESETS: Record<string, ProductItem[]> = {
supermarket: [
{ id: 'sm1', name: 'Premium Basmati Rice (1kg)', price: 120, stock: 45, code: '89012300052' },
{ id: 'sm2', name: 'Fresh Farm Milk (1L)', price: 65, stock: 2, code: '89045300010' },
{ id: 'sm3', name: 'Refined Cooking Oil (1L)', price: 165, stock: 18, code: '89010320491' },
{ id: 'sm4', name: 'Refined White Sugar (1kg)', price: 48, stock: 3, code: '89034200421' },
{ id: 'sm5', name: 'Organic Green T-Tea (250g)', price: 110, stock: 24, code: '89512300011' },
{ id: 'sm6', name: 'Whole Wheat Atta (5kg)', price: 230, stock: 7, code: '89011210042' }
],
restaurant: [
{ id: 'rt1', name: 'Fresh Paneer Butter Masala', price: 240, stock: 19, code: '9901230' },
{ id: 'rt2', name: 'Butter Tandoori Naan', price: 45, stock: 15, code: '9904530' },
{ id: 'rt3', name: 'Dum Veg Biryani Deluxe', price: 190, stock: 2, code: '9901032' },
{ id: 'rt4', name: 'Crispy Butter Masala Dosa', price: 90, stock: 22, code: '9903420' },
{ id: 'rt5', name: 'Rich Chocolate Milkshake', price: 120, stock: 3, code: '9951230' },
{ id: 'rt6', name: 'Hot Roasted Filter Coffee', price: 50, stock: 30, code: '9901121' }
],
clothing: [
{ id: 'cl1', name: 'Premium Oxford Cotton Shirt', price: 899, stock: 8, code: '77012301' },
{ id: 'cl2', name: 'Slim Fit Dark Jeans', price: 1599, stock: 2, code: '77045302' },
{ id: 'cl3', name: 'Designer Summer Slip Dress', price: 1299, stock: 5, code: '77010323' },
{ id: 'cl4', name: 'Graphic Printed Tee-Shirt', price: 450, stock: 40, code: '77034204' },
{ id: 'cl5', name: 'Breathable Sports Sneakers', price: 1899, stock: 3, code: '77512305' },
{ id: 'cl6', name: 'Stitch Leather Belt Brown', price: 349, stock: 12, code: '77011216' }
],
kirana: [
{ id: 'kr1', name: 'Premium Toor Dal (1kg)', price: 145, stock: 15, code: '4401230' },
{ id: 'kr2', name: 'Iodized Powdered Salt (1kg)', price: 22, stock: 50, code: '4404530' },
{ id: 'kr3', name: 'Herbal Moisturizer Soap', price: 42, stock: 2, code: '4401032' },
{ id: 'kr4', name: 'Anti-Germ Toothpaste (200g)', price: 85, stock: 16, code: '4403420' },
{ id: 'kr5', name: 'Dishwash Cleaning Soap', price: 25, stock: 3, code: '4451230' },
{ id: 'kr6', name: 'Red Chili Powder (200g)', price: 65, stock: 18, code: '4401121' }
],
others: [
{ id: 'ot1', name: 'Utility Household Scrubber', price: 35, stock: 33, code: '5501230' },
{ id: 'ot2', name: 'AAA Rechargeable Batteries', price: 120, stock: 4, code: '5504530' },
{ id: 'ot3', name: 'LED High Bright Focus Bulb', price: 140, stock: 2, code: '5501032' },
{ id: 'ot4', name: 'Water Bottle Stainless (1L)', price: 320, stock: 11, code: '5503420' },
{ id: 'ot5', name: 'Microfiber Floor Duster', price: 75, stock: 24, code: '5551230' },
{ id: 'ot6', name: 'Multi-plug Protection Adapter', price: 240, stock: 9, code: '5501121' }
]
};
export default function BillingDashboard({ state, onReset }: BillingDashboardProps) {
const currentCategory = state.businessType || 'supermarket';
const products = INVENTORY_PRESETS[currentCategory] || INVENTORY_PRESETS.supermarket;
const [billingList, setBillingList] = useState<BillingItem[]>([]);
const [printingInvoice, setPrintingInvoice] = useState<boolean>(false);
const [invoiceNumber, setInvoiceNumber] = useState<string>('POZO-26402');
const [scanMessage, setScanMessage] = useState<string>('');
const handleAddItem = (product: ProductItem) => {
const existing = billingList.find((item) => item.product.id === product.id);
if (existing) {
if (existing.quantity >= product.stock && state.features?.inventoryAlerts) {
alert(`Cannot add more. Inventory limit reached for ${product.name}!`);
return;
}
setBillingList(
billingList.map((item) =>
item.product.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
);
} else {
setBillingList([...billingList, { product, quantity: 1 }]);
}
};
const handleRemoveOne = (productId: string) => {
const existing = billingList.find((item) => item.product.id === productId);
if (existing && existing.quantity > 1) {
setBillingList(
billingList.map((item) =>
item.product.id === productId
? { ...item, quantity: item.quantity - 1 }
: item
)
);
} else {
setBillingList(billingList.filter((item) => item.product.id !== productId));
}
};
// Simulating Barcode scanning if enabled
const simulateBarcodeScan = () => {
if (!state.features?.barcodeScanner) return;
// Pick a random product from inventory catalog
const randomProduct = products[Math.floor(Math.random() * products.length)];
handleAddItem(randomProduct);
setScanMessage(`🎯 Scanned EAN Barcode [${randomProduct.code}] - Added ${randomProduct.name}!`);
setTimeout(() => {
setScanMessage('');
}, 3500);
};
// Reset actual register
const clearRegister = () => {
setBillingList([]);
};
// Financial calculations
const subtotal = billingList.reduce((acc, item) => acc + item.product.price * item.quantity, 0);
const hasGstin = state.gstin && state.gstin.trim().length > 0;
// 18% GST (9% CGST + 9% SGST) split if GSTIN specified
const cgstRate = 0.09;
const sgstRate = 0.09;
const cgst = hasGstin ? subtotal * cgstRate : 0;
const sgst = hasGstin ? subtotal * sgstRate : 0;
const total = subtotal + cgst + sgst;
const handlePrint = () => {
const randNum = `POZO-${Math.floor(10000 + Math.random() * 90000)}`;
setInvoiceNumber(randNum);
setPrintingInvoice(true);
};
const closeReceiptAndReset = () => {
setPrintingInvoice(false);
clearRegister();
};
return (
<div className="flex-1 flex flex-col h-full bg-slate-900 text-white relative select-none animate-fade-in-up font-sans">
{/* Dynamic Header */}
<header className="px-margin-page py-3.5 bg-neutral-900 border-b border-neutral-800 flex items-center justify-between shadow-md z-20 shrink-0">
<div className="flex items-center gap-2 text-left">
<div className="w-9 h-9 rounded-xl bg-primary flex items-center justify-center shadow-md shrink-0">
<span className="text-white font-display font-extrabold text-sm">P</span>
</div>
<div>
<h1 className="font-display font-extrabold text-sm tracking-tight line-clamp-1">
{state.businessName || 'Sharma General Store'}
</h1>
<p className="text-[10px] text-neutral-400 capitalize flex items-center gap-1 font-mono">
<span className="inline-block w-1.5 h-1.5 bg-emerald-500 rounded-full"></span>
Live Register
</p>
</div>
</div>
{/* Feature status indicators on top bar */}
<div className="flex items-center gap-1.5">
{state.features?.barcodeScanner && (
<button
onClick={simulateBarcodeScan}
className="p-2 bg-neutral-800 hover:bg-neutral-700 active:scale-95 text-primary rounded-xl transition-all border border-neutral-700 shrink-0 flex items-center gap-1 relative group"
title="Click to simulate scanning a barcode"
>
<Camera className="w-4 h-4 text-primary animate-[pulse_2s_infinite]" />
<span className="text-[10px] uppercase font-bold text-neutral-300 hidden sm:inline">Scan Item</span>
</button>
)}
{state.features?.kotManagement && (
<span className="text-[9px] bg-sky-500/10 text-sky-400 border border-sky-500/20 px-2 py-1 rounded-lg font-bold uppercase shrink-0">
KOT Active
</span>
)}
<button
onClick={onReset}
className="p-2 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-xl transition-colors border border-neutral-700 shrink-0"
title="Return to Config screens"
>
<ArrowLeft className="w-4 h-4" />
</button>
</div>
</header>
{/* Barcode feedback banner */}
{scanMessage && (
<div className="bg-emerald-500 text-neutral-900 py-1.5 px-4 text-center text-xs font-bold font-sans animate-slide-left z-30 flex items-center justify-center gap-1 shadow-md">
<Sparkles className="w-3.5 h-3.5 shrink-0" />
<span>{scanMessage}</span>
</div>
)}
{/* Content Columns layout */}
<main className="flex-1 overflow-hidden flex flex-col sm:flex-row divide-y sm:divide-y-0 sm:divide-x divide-neutral-800">
{/* Left: Product Catalog layout */}
<section className="flex-1 overflow-y-auto no-scrollbar p-4 flex flex-col min-h-[50%] sm:min-h-0">
<div className="flex items-center justify-between mb-3 shrink-0 text-left">
<h3 className="text-xs font-bold uppercase text-neutral-400 tracking-wider">Store Catalog</h3>
<span className="text-[10px] bg-neutral-800 text-neutral-400 font-mono px-2 py-0.5 rounded-full border border-neutral-700/50 capitalize">
{currentCategory} preset
</span>
</div>
<div className="grid grid-cols-2 gap-2.5">
{products.map((prod) => {
const currentInCart = billingList.find(b => b.product.id === prod.id);
const countInCart = currentInCart?.quantity || 0;
const isLowStock = state.features?.inventoryAlerts && (prod.stock - countInCart) <= 3;
const remainingStock = prod.stock - countInCart;
return (
<button
key={prod.id}
onClick={() => handleAddItem(prod)}
disabled={remainingStock <= 0 && state.features?.inventoryAlerts}
className={`p-3 rounded-2xl bg-neutral-800/60 border hover:bg-neutral-800 hover:border-neutral-700 transition-all text-left flex flex-col justify-between h-[115px] cursor-pointer shadow-sm relative group ${
countInCart > 0 ? 'border-primary ring-1 ring-primary/30' : 'border-neutral-800'
} ${remainingStock <= 0 && state.features?.inventoryAlerts ? 'opacity-40 cursor-not-allowed' : ''}`}
>
<div className="w-full text-left">
<h4 className="font-sans font-bold text-xs leading-snug text-neutral-100 line-clamp-2 group-hover:text-white">{prod.name}</h4>
<p className="font-mono font-bold text-primary text-[13px] mt-1">{prod.price}</p>
</div>
{/* Stock countdown if Alert feature active in Step 5 */}
<div className="flex items-center justify-between w-full mt-2">
{state.features?.inventoryAlerts ? (
<span className={`text-[9px] font-bold font-mono px-1.5 py-0.5 rounded ${
isLowStock
? 'bg-rose-500/10 text-rose-400 animate-pulse border border-rose-500/20'
: 'bg-neutral-700/50 text-neutral-400'
}`}>
{remainingStock <= 0 ? 'Out of stock' : `${remainingStock} Stock`}
</span>
) : (
<div className="h-1" />
)}
{countInCart > 0 && (
<span className="text-[10px] bg-primary text-white font-bold w-5 h-5 rounded-full flex items-center justify-center shadow-md">
{countInCart}
</span>
)}
</div>
</button>
);
})}
</div>
</section>
{/* Right: Checkout Ledger Tray */}
<section className="w-full sm:w-[320px] bg-neutral-900/40 shrink-0 p-4 flex flex-col justify-between overflow-hidden relative min-h-[50%] sm:min-h-0">
<div className="flex-1 overflow-hidden flex flex-col">
<div className="flex items-center justify-between mb-3.5 shrink-0 text-left">
<h3 className="text-xs font-bold uppercase text-neutral-400 tracking-wider flex items-center gap-1.5">
<ShoppingCart className="w-4 h-4 text-neutral-400" />
Billing Tray ({billingList.length})
</h3>
{billingList.length > 0 && (
<button
type="button"
onClick={clearRegister}
className="text-[10px] text-neutral-400 hover:text-primary transition-colors cursor-pointer font-bold font-sans flex items-center gap-1"
>
<RefreshCw className="w-3 h-3" />
Clear List
</button>
)}
</div>
{/* Selected products scroll tray */}
<div className="flex-grow overflow-y-auto no-scrollbar space-y-2 pb-4">
{billingList.length === 0 ? (
<div className="h-[120px] sm:h-full flex flex-col items-center justify-center text-center p-4">
<div className="w-11 h-11 bg-neutral-800 rounded-full flex items-center justify-center text-neutral-500 mb-2">
<ShoppingCart className="w-5 h-5" />
</div>
<p className="text-[11px] text-neutral-400 font-sans leading-relaxed">No products selected from Catalog above.</p>
<p className="text-[10px] text-neutral-500 font-sans mt-0.5">Click catalog items to start ledger computations.</p>
</div>
) : (
billingList.map((item) => (
<div
key={item.product.id}
className="p-3 rounded-xl bg-neutral-800/40 border border-neutral-800/80 flex items-center justify-between gap-1 animate-slide-left"
>
<div className="flex-grow text-left">
<p className="text-xs font-medium text-neutral-100 line-clamp-1 leading-none mb-1">{item.product.name}</p>
<span className="text-[10px] font-mono text-neutral-400 font-bold">{item.product.price} × {item.quantity}</span>
</div>
{/* Quantity controls */}
<div className="flex items-center bg-neutral-800 rounded-xl px-1.5 py-1 shrink-0 border border-neutral-700/40 ml-1.5">
<button
onClick={() => handleRemoveOne(item.product.id)}
className="p-1 hover:bg-neutral-700 hover:text-white rounded text-neutral-400 transition-colors shrink-0"
>
<Minus className="w-3.5 h-3.5" />
</button>
<span className="font-mono font-bold text-xs text-neutral-100 px-2 shrink-0">{item.quantity}</span>
<button
onClick={() => handleAddItem(item.product)}
className="p-1 hover:bg-neutral-700 hover:text-white rounded text-neutral-400 transition-colors shrink-0"
>
<Plus className="w-3.5 h-3.5" />
</button>
</div>
</div>
))
)}
</div>
</div>
{/* Checkout pricing sum list */}
<div className="border-t border-neutral-800 pt-3 space-y-2 shrink-0 z-10 text-left bg-neutral-900/80 sm:bg-transparent -mx-4 -mb-4 p-4 sm:p-0">
<div className="space-y-1.5 font-sans">
<div className="flex justify-between items-center text-xs text-neutral-400">
<span>Subtotal</span>
<span className="font-mono font-bold">{subtotal.toFixed(2)}</span>
</div>
{hasGstin && (
<>
<div className="flex justify-between items-center text-[11px] text-neutral-500 ml-1">
<span>CGST (9.0%)</span>
<span className="font-mono">{cgst.toFixed(2)}</span>
</div>
<div className="flex justify-between items-center text-[11px] text-neutral-500 ml-1">
<span>SGST (9.0%)</span>
<span className="font-mono">{sgst.toFixed(2)}</span>
</div>
</>
)}
<div className="h-[1px] bg-neutral-800" />
<div className="flex justify-between items-center text-sm font-extrabold text-neutral-100">
<span>Grand Total</span>
<span className="font-mono text-primary">{total.toFixed(2)}</span>
</div>
</div>
<button
onClick={handlePrint}
disabled={billingList.length === 0}
className={`w-full h-11 font-display font-bold text-xs rounded-xl flex items-center justify-center gap-1.5 mt-3 transition-transform active:scale-[0.98] ${
billingList.length > 0
? 'bg-primary hover:bg-primary-container text-white shadow-lg shadow-primary/20'
: 'bg-neutral-800 text-neutral-500 cursor-not-allowed shadow-none'
}`}
>
<Receipt className="w-4 h-4" />
<span>Review & Generate Invoice</span>
</button>
</div>
</section>
</main>
{/* High Fidelity Printable thermal invoice popup */}
{printingInvoice && (
<div className="absolute inset-0 bg-black/85 backdrop-blur-sm flex items-center justify-center p-4 z-50 overflow-y-auto no-scrollbar select-text">
<div className="w-full max-w-[340px] bg-white text-neutral-900 rounded-2xl shadow-2xl p-5 flex flex-col gap-4 animate-pop-in border border-neutral-200">
{/* Bill receipt frame */}
<div className="flex-1 flex flex-col p-1.5 font-mono text-[11px] tracking-tight leading-relaxed text-left border-dashed border-2 border-neutral-300">
{/* Receipt Header logo */}
<div className="text-center font-bold mb-4">
<h4 className="text-[17px] font-black uppercase tracking-tight text-neutral-900">{state.businessName || 'Sharma General Store'}</h4>
<p className="text-[10px] text-neutral-500 font-sans mt-0.5 capitalize">{currentCategory} Service Center</p>
{hasGstin && (
<p className="text-[9px] mt-1.5 font-mono bg-neutral-100 py-0.5 px-2.5 rounded inline-block">
GSTIN: {state.gstin}
</p>
)}
</div>
{/* Dividing dash lines */}
<div className="border-b border-dashed border-neutral-300 mb-2.5" />
{/* Receipt details columns */}
<div className="space-y-1 mb-3 font-semibold text-neutral-600">
<div className="flex justify-between">
<span>Invoice No:</span>
<span className="text-neutral-900 font-extrabold">{invoiceNumber}</span>
</div>
<div className="flex justify-between">
<span>Date Track:</span>
<span>{new Date().toLocaleDateString('en-IN')}</span>
</div>
<div className="flex justify-between">
<span>Terminal Class:</span>
<span>Web App Counter</span>
</div>
</div>
{/* Dash */}
<div className="border-b border-dashed border-neutral-300 mb-2" />
{/* Table labels */}
<div className="grid grid-cols-12 gap-1 font-extrabold text-neutral-900 uppercase tracking-widest text-[9px] mb-1.5">
<span className="col-span-6">Item Description</span>
<span className="col-span-2 text-right">Qty</span>
<span className="col-span-4 text-right">Total</span>
</div>
{/* Items row columns */}
<div className="space-y-1.5 mb-3 text-neutral-600">
{billingList.map(b => (
<div key={b.product.id} className="grid grid-cols-12 gap-1 font-semibold">
<span className="col-span-6 line-clamp-2">{b.product.name}</span>
<span className="col-span-2 text-right">×{b.quantity}</span>
<span className="col-span-4 text-right font-mono font-bold text-neutral-800">{b.product.price * b.quantity}</span>
</div>
))}
</div>
{/* Dash */}
<div className="border-b border-dashed border-neutral-300 mb-2.5" />
{/* Pricing ledger splits */}
<div className="space-y-1 text-neutral-600 font-bold mb-3">
<div className="flex justify-between">
<span>Cart Amount:</span>
<span className="font-mono">{subtotal.toFixed(2)}</span>
</div>
{hasGstin && (
<>
<div className="flex justify-between text-neutral-500 text-[10px] pl-3">
<span>CGST (9%):</span>
<span className="font-mono">{cgst.toFixed(2)}</span>
</div>
<div className="flex justify-between text-neutral-500 text-[10px] pl-3">
<span>SGST (9%):</span>
<span className="font-mono">{sgst.toFixed(2)}</span>
</div>
</>
)}
<div className="border-b border-dashed border-neutral-200 my-1" />
<div className="flex justify-between text-neutral-900 text-sm font-extrabold">
<span>NET TOTAL:</span>
<span className="font-mono">{total.toFixed(2)}</span>
</div>
</div>
{/* Receipt Footer */}
<div className="border-b border-dashed border-neutral-300 mb-2.5" />
<div className="text-center text-[10px] text-neutral-500 font-sans space-y-1 pt-1 font-medium">
<p>Thank You For Your Patronage!</p>
<div className="flex items-center justify-center gap-1 font-bold text-[#af101a] mt-2 bg-[#ffdad6] py-1 px-3.5 rounded-full inline-flex">
<ShieldCheck className="w-3.5 h-3.5 text-[#af101a]" />
<span>Powered by PozoApp</span>
</div>
</div>
</div>
{/* Print trigger actions */}
<div className="flex flex-col gap-2 font-display">
<button
onClick={() => {
alert(`🧾 Simulated invoice print successful! Generated invoice standard output code ${invoiceNumber}.`);
}}
className="w-full h-11 bg-neutral-900 text-white font-bold text-xs rounded-xl flex items-center justify-center gap-1.5 shadow hover:bg-neutral-850"
>
<Printer className="w-4 h-4 text-emerald-400" />
<span>Simulate Print Reciept</span>
</button>
<button
onClick={closeReceiptAndReset}
className="w-full h-11 bg-primary text-white font-bold text-xs rounded-xl flex items-center justify-center gap-1.5 shadow"
>
<CheckCircle2 className="w-4 h-4 text-white" />
<span>New Ledger Sale</span>
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,641 @@
import React, { useState } from 'react';
import {
ArrowLeft, ShoppingCart, UtensilsCrossed, Shirt, ShoppingBag, LayoutGrid, HelpCircle,
Store, MapPin, Camera, Check, ChevronRight, FileText, Utensils, Award, CreditCard,
UploadCloud, X, Image as ImageIcon
} from 'lucide-react';
import { OnboardingState, BUSINESS_TYPES } from '../types';
interface BusinessSetupProps {
onBack: () => void;
onContinue: (data: { businessName: string; businessType: string; gstin: string }) => void;
state: OnboardingState;
}
export default function BusinessSetupScreen({ onBack, onContinue, state }: BusinessSetupProps) {
const [name, setName] = useState(state.businessName || '');
const [type, setType] = useState(state.businessType || 'supermarket');
const [gstin, setGstin] = useState(state.gstin || '');
const [errorHeader, setErrorHeader] = useState('');
// High-fidelity interactive state matching and extending spec
const [selectedDocs, setSelectedDocs] = useState<string[]>(['GSTIN', 'FSSAI']);
const [detectedLocation, setDetectedLocation] = useState<string>('');
const [facadePhotoUploaded, setFacadePhotoUploaded] = useState<boolean>(false);
const [gstinVerified, setGstinVerified] = useState<boolean>(false);
const [toastMessage, setToastMessage] = useState<string>('');
// Interactive local states for each optional document form
const [docNumbers, setDocNumbers] = useState<Record<string, string>>({
GSTIN: state.gstin || '',
FSSAI: '',
Aadhar: '',
'Udhyog Aadhar': '',
PAN: ''
});
const [uploadedDocs, setUploadedDocs] = useState<Record<string, string>>({});
const triggerToast = (msg: string) => {
setToastMessage(msg);
setTimeout(() => {
setToastMessage('');
}, 3200);
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
if (e.target.value.trim()) {
setErrorHeader('');
}
};
const handleGstinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
if (value.length <= 15) {
setGstin(value);
setDocNumbers(prev => ({ ...prev, GSTIN: value }));
}
};
const handleDocNumberChange = (doc: string, val: string) => {
setDocNumbers(prev => ({ ...prev, [doc]: val }));
if (doc === 'GSTIN') {
setGstin(val.toUpperCase().replace(/[^A-Z0-9]/g, ''));
}
};
const toggleDoc = (doc: string) => {
if (selectedDocs.includes(doc)) {
setSelectedDocs(prev => prev.filter(d => d !== doc));
} else {
setSelectedDocs(prev => [...prev, doc]);
}
// Simple haptic feedback simulation
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(20);
}
};
const handleMockUpload = (doc: string) => {
const mockFileNames: Record<string, string> = {
GSTIN: 'gst_certificate_signed.pdf',
FSSAI: 'fssai_license_verified.pdf',
Aadhar: 'aadhar_card_front_back.jpg',
'Udhyog Aadhar': 'msme_reg_certificate.pdf',
PAN: 'pan_card_copy_cropped.jpg'
};
setUploadedDocs(prev => ({
...prev,
[doc]: mockFileNames[doc] || 'document_uploaded.pdf'
}));
triggerToast(`📸 ${doc} proof attached successfully!`);
};
const clearDocUpload = (doc: string) => {
setUploadedDocs(prev => {
const copy = { ...prev };
delete copy[doc];
return copy;
});
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) {
setErrorHeader('Please specify your Business/Shop Name');
return;
}
onContinue({ businessName: name.trim(), businessType: type, gstin });
};
const getCategoryIcon = (id: string, isSelected: boolean) => {
const sizeClass = "w-7 h-7 stroke-[1.8]";
const colorClass = isSelected ? "text-primary" : "text-on-surface-variant";
switch (id) {
case 'supermarket':
return <ShoppingCart className={`${sizeClass} ${colorClass}`} />;
case 'restaurant':
return <UtensilsCrossed className={`${sizeClass} ${colorClass}`} />;
case 'clothing':
return <Shirt className={`${sizeClass} ${colorClass}`} />;
case 'kirana':
return <ShoppingBag className={`${sizeClass} ${colorClass}`} />;
case 'grocers':
return <Store className={`${sizeClass} ${colorClass}`} />;
case 'others':
default:
return <LayoutGrid className={`${sizeClass} ${colorClass}`} />;
}
};
const isFormValid = name.trim().length > 0;
return (
<div className="flex-1 flex flex-col h-full bg-background relative select-none animate-fade-in-up">
{/* App bar */}
<header className="flex items-center justify-between px-margin-page h-[56px] border-b border-surface-container-high/60 bg-white/80 backdrop-blur z-20">
<button
onClick={onBack}
className="p-1 px-1.5 -ml-1 rounded-full text-on-surface-variant hover:bg-surface-container-low transition-colors"
aria-label="Back"
id="btn-business-back"
>
<ArrowLeft className="w-5 h-5 text-primary" strokeWidth={2.5} />
</button>
<span className="font-display font-extrabold text-[17px] text-primary tracking-tight">PozoApp</span>
<span className="text-on-surface-variant font-sans text-[10px] font-semibold">Step 3 of 5</span>
</header>
{/* Progress slider bar: 60% filled */}
<div className="w-full h-1.5 bg-surface-container-highest flex border-b border-surface-container-high/20">
<div className="h-full bg-primary w-[60%] rounded-full transition-all duration-300"></div>
</div>
{/* Toast Alert */}
{toastMessage && (
<div className="fixed top-16 left-1/2 -translate-x-1/2 z-50 bg-neutral-900 text-white text-xs px-4 py-3 rounded-full shadow-lg flex items-center gap-2 max-w-[90%] whitespace-nowrap animate-pop-in">
<span>{toastMessage}</span>
</div>
)}
{/* Form Area */}
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto no-scrollbar pt-6 pb-36 px-margin-page bg-pattern-fine flex flex-col">
<div className="space-y-6 max-w-xl mx-auto w-full">
{/* Header titles */}
<div className="space-y-1 text-left">
<h2 className="font-display font-bold text-2xl text-on-surface leading-tight">Complete verification</h2>
<p className="font-sans text-xs sm:text-sm text-on-surface-variant leading-relaxed">
Verify your business details to start selling.
</p>
</div>
{/* Form details */}
<div className="space-y-5">
{/* Input 1: Shop Name */}
<div className="space-y-1.5 text-left">
<label htmlFor="input-shop-name" className="text-xs font-bold text-on-surface uppercase tracking-wide flex items-center justify-between">
<span>Business/Shop Name <span className="text-error">*</span></span>
</label>
<input
type="text"
id="input-shop-name"
placeholder="e.g. Sharma General Store"
value={name}
onChange={handleNameChange}
className="w-full h-[54px] px-4 rounded-xl border border-outline bg-surface-container-lowest text-on-surface font-sans font-medium text-sm sm:text-base focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none transition-shadow placeholder:text-outline/65 shadow-sm"
/>
{errorHeader && <p className="text-xs text-error font-medium pl-1">{errorHeader}</p>}
</div>
{/* Input 2: Business Type Selection Grid */}
<div className="space-y-2 text-left">
<label className="text-xs font-bold text-on-surface uppercase tracking-wide block">
Business Type <span className="text-error">*</span>
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2.5" id="categoryGrid">
{BUSINESS_TYPES.map((cat) => {
const isSelected = type === cat.id;
return (
<button
key={cat.id}
type="button"
onClick={() => {
setType(cat.id);
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(50);
}
}}
className={`category-card flex flex-col items-center justify-center gap-1.5 p-3 rounded-xl border transition-all duration-200 cursor-pointer h-[100px] ${
isSelected
? 'border-primary bg-primary-fixed text-on-primary-fixed ring-1 ring-primary shadow-sm'
: 'border-outline-variant bg-surface-container-lowest text-on-surface-variant hover:bg-neutral-50/85'
}`}
>
<div className={`icon-container transition-transform duration-300 ${isSelected ? 'scale-115 text-primary' : 'text-on-surface-variant'}`}>
{getCategoryIcon(cat.id, isSelected)}
</div>
<span className="font-sans font-semibold text-[13px] text-center leading-tight">{cat.name}</span>
</button>
);
})}
</div>
</div>
{/* Input 3: GSTIN (With Verify Button) */}
<div className="space-y-1.5 text-left pt-1">
<div className="flex justify-between items-center">
<label htmlFor="input-gstin" className="text-xs font-bold text-on-surface uppercase tracking-wide">
GSTIN
</label>
<span className="text-on-surface-variant/70 font-semibold text-[11px] font-sans italic">Optional</span>
</div>
<div className="flex gap-2">
<input
type="text"
id="input-gstin"
maxLength={15}
placeholder="15-digit GST Number"
value={gstin}
onChange={handleGstinChange}
className="flex-1 h-[54px] px-4 rounded-xl border border-outline bg-surface-container-lowest text-on-surface font-sans font-bold text-sm tracking-widest leading-none focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none transition-shadow placeholder:text-outline-variant/60 shadow-sm uppercase"
/>
<button
type="button"
onClick={() => {
if (gstin.length < 15) {
triggerToast('⚠️ Please enter a 15-digit GSTIN number first');
return;
}
setGstinVerified(true);
triggerToast('✅ GSTIN Verified Successfully!');
}}
className={`h-[54px] px-5 rounded-xl font-sans font-bold text-sm transition-all shadow-sm cursor-pointer whitespace-nowrap flex items-center justify-center gap-1.5 ${
gstinVerified
? 'bg-green-700 hover:bg-green-800 text-white shadow-md'
: 'bg-secondary hover:bg-on-secondary-fixed-variant text-white'
}`}
>
{gstinVerified ? <Check className="w-4 h-4 text-white stroke-[2.5]" /> : null}
{gstinVerified ? 'Verified' : 'Verify'}
</button>
</div>
<p className="text-[12px] text-on-surface-variant font-sans flex items-center gap-1.5 pl-0.5 mt-1">
<HelpCircle className="w-3.5 h-3.5 text-on-surface-variant" />
Auto-fetches address & legal name on verification.
</p>
</div>
{/* Input 4: Shop Location Detect */}
<div className="space-y-1.5 text-left pt-1">
<label className="text-xs font-bold text-on-surface uppercase tracking-wide block">
Shop Location <span className="text-error">*</span>
</label>
<button
type="button"
onClick={() => {
setDetectedLocation('Sector 62, Noida, Uttar Pradesh, 201301');
triggerToast('📍 Location detected: Sector 62, Noida, UP');
}}
className="w-full flex items-center gap-3.5 p-4 rounded-xl border border-outline-variant border-dashed bg-surface-container-lowest hover:bg-neutral-50/60 transition-all cursor-pointer text-left shadow-sm group"
>
<div className="p-2.5 bg-secondary-fixed text-on-secondary-fixed rounded-full">
<MapPin className="w-5 h-5 text-secondary" strokeWidth={2.3} />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-sans font-bold text-[14px] text-on-surface">
{detectedLocation ? 'Location Pinpoint Success' : 'Detect current location'}
</h4>
<p className="font-sans text-[12px] text-on-surface-variant/80 mt-0.5 truncate">
{detectedLocation ? `📍 ${detectedLocation}` : 'Pin shop address automatically'}
</p>
</div>
<ChevronRight className="w-4 h-4 text-on-surface-variant/40 group-hover:translate-x-0.5 transition-transform" />
</button>
</div>
{/* Input 5: Identity Proof Section - WITH SELECT ALL THAT APPLY & CUSTOM ICONS */}
<div className="space-y-3.5 text-left pt-1">
<label className="text-xs font-bold text-on-surface uppercase tracking-wide block">
Identity / License Documents <span className="text-on-surface-variant font-normal">(Select all that apply)</span>
</label>
<div className="flex flex-wrap gap-2">
{[
{ name: 'GSTIN', icon: 'FileText' },
{ name: 'FSSAI', icon: 'Utensils' },
{ name: 'Aadhar', icon: 'Award' },
{ name: 'Udhyog Aadhar', icon: 'Store' },
{ name: 'PAN', icon: 'CreditCard' }
].map((doc) => {
const isSelected = selectedDocs.includes(doc.name);
return (
<button
key={doc.name}
type="button"
onClick={() => toggleDoc(doc.name)}
className={`doc-chip px-4 py-2.5 rounded-full border text-[13px] font-bold transition-all flex items-center gap-2 cursor-pointer ${
isSelected
? 'border-primary bg-primary text-white shadow-sm ring-1 ring-primary'
: 'border-outline-variant bg-surface-container-lowest text-on-surface hover:bg-neutral-50'
}`}
>
{doc.name === 'GSTIN' && <FileText className="w-3.5 h-3.5" />}
{doc.name === 'FSSAI' && <Utensils className="w-3.5 h-3.5" />}
{doc.name === 'Aadhar' && <Award className="w-3.5 h-3.5" />}
{doc.name === 'Udhyog Aadhar' && <Store className="w-3.5 h-3.5" />}
{doc.name === 'PAN' && <CreditCard className="w-3.5 h-3.5" />}
<span>{doc.name}</span>
</button>
);
})}
</div>
{/* Dynamic Subforms Containers matching the collapsed/expanded design */}
<div className="space-y-3 mt-3" id="docInputsContainer">
{/* GSTIN Details Container */}
{selectedDocs.includes('GSTIN') && (
<div className="border border-outline-variant rounded-xl p-4 bg-surface-container-lowest animate-fade-in-down">
<div className="flex items-center justify-between mb-3 border-b border-neutral-100 pb-2">
<span className="font-sans font-bold text-sm text-on-surface flex items-center gap-2">
<FileText className="w-4 h-4 text-primary" /> GSTIN Details
</span>
<button
type="button"
onClick={() => toggleDoc('GSTIN')}
className="text-error hover:text-red-700 transition-colors p-0.5 rounded-full hover:bg-neutral-100"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="flex flex-col gap-3">
<input
type="text"
placeholder="Enter GSTIN Number (15-digit)"
value={docNumbers.GSTIN}
onChange={(e) => handleDocNumberChange('GSTIN', e.target.value)}
className="h-[46px] px-3.5 rounded-lg border border-outline bg-surface-container-lowest text-on-surface font-sans text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary placeholder:text-outline-variant"
/>
{uploadedDocs.GSTIN ? (
<div className="flex items-center justify-between p-2 px-3 border border-green-200 bg-green-50/50 rounded-lg text-xs font-semibold text-green-800">
<span className="truncate flex items-center gap-1.5">
<Check className="w-4 h-4 text-green-700 stroke-[3]" />
{uploadedDocs.GSTIN}
</span>
<button type="button" onClick={() => clearDocUpload('GSTIN')} className="text-red-500 hover:text-red-700 font-bold ml-2">Remove</button>
</div>
) : (
<button
type="button"
onClick={() => handleMockUpload('GSTIN')}
className="flex items-center justify-center gap-2 h-11 w-full border-2 border-dashed border-secondary hover:bg-secondary/5 text-secondary rounded-lg font-sans font-bold text-xs transition-colors cursor-pointer"
>
<Camera className="w-4 h-4" />
Scan or Upload GST Certificate
</button>
)}
</div>
</div>
)}
{/* FSSAI Details Container */}
{selectedDocs.includes('FSSAI') && (
<div className="border border-outline-variant rounded-xl p-4 bg-surface-container-lowest animate-fade-in-down">
<div className="flex items-center justify-between mb-3 border-b border-neutral-100 pb-2">
<span className="font-sans font-bold text-sm text-on-surface flex items-center gap-2">
<Utensils className="w-4 h-4 text-primary" /> FSSAI License Details
</span>
<button
type="button"
onClick={() => toggleDoc('FSSAI')}
className="text-error hover:text-red-700 transition-colors p-0.5 rounded-full hover:bg-neutral-100"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="flex flex-col gap-3">
<input
type="text"
placeholder="14-digit FSSAI Number"
value={docNumbers.FSSAI}
onChange={(e) => handleDocNumberChange('FSSAI', e.target.value)}
className="h-[46px] px-3.5 rounded-lg border border-outline bg-surface-container-lowest text-on-surface font-sans text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary placeholder:text-outline-variant"
/>
{uploadedDocs.FSSAI ? (
<div className="flex items-center justify-between p-2 px-3 border border-green-200 bg-green-50/50 rounded-lg text-xs font-semibold text-green-800">
<span className="truncate flex items-center gap-1.5">
<Check className="w-4 h-4 text-green-700 stroke-[3]" />
{uploadedDocs.FSSAI}
</span>
<button type="button" onClick={() => clearDocUpload('FSSAI')} className="text-red-500 hover:text-red-700 font-bold ml-2">Remove</button>
</div>
) : (
<button
type="button"
onClick={() => handleMockUpload('FSSAI')}
className="flex items-center justify-center gap-2 h-11 w-full border-2 border-dashed border-secondary hover:bg-secondary/5 text-secondary rounded-lg font-sans font-bold text-xs transition-colors cursor-pointer"
>
<UploadCloud className="w-4 h-4" />
Scan or Upload FSSAI License
</button>
)}
</div>
</div>
)}
{/* Aadhar Details Container */}
{selectedDocs.includes('Aadhar') && (
<div className="border border-outline-variant rounded-xl p-4 bg-surface-container-lowest animate-fade-in-down">
<div className="flex items-center justify-between mb-3 border-b border-neutral-100 pb-2">
<span className="font-sans font-bold text-sm text-on-surface flex items-center gap-2">
<Award className="w-4 h-4 text-primary" /> Aadhar Card Details
</span>
<button
type="button"
onClick={() => toggleDoc('Aadhar')}
className="text-error hover:text-red-700 transition-colors p-0.5 rounded-full hover:bg-neutral-100"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="flex flex-col gap-3">
<input
type="text"
maxLength={12}
placeholder="12-digit Aadhar Number"
value={docNumbers.Aadhar}
onChange={(e) => handleDocNumberChange('Aadhar', e.target.value.replace(/\D/g, ''))}
className="h-[46px] px-3.5 rounded-lg border border-outline bg-surface-container-lowest text-on-surface font-sans text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary placeholder:text-outline-variant"
/>
{uploadedDocs.Aadhar ? (
<div className="flex items-center justify-between p-2 px-3 border border-green-200 bg-green-50/50 rounded-lg text-xs font-semibold text-green-800">
<span className="truncate flex items-center gap-1.5">
<Check className="w-4 h-4 text-green-700 stroke-[3]" />
{uploadedDocs.Aadhar}
</span>
<button type="button" onClick={() => clearDocUpload('Aadhar')} className="text-red-500 hover:text-red-700 font-bold ml-2">Remove</button>
</div>
) : (
<button
type="button"
onClick={() => handleMockUpload('Aadhar')}
className="flex items-center justify-center gap-2 h-11 w-full border-2 border-dashed border-secondary hover:bg-secondary/5 text-secondary rounded-lg font-sans font-bold text-xs transition-colors cursor-pointer"
>
<Camera className="w-4 h-4" />
Upload Front & Back Photo
</button>
)}
</div>
</div>
)}
{/* MSME Details Container */}
{selectedDocs.includes('Udhyog Aadhar') && (
<div className="border border-outline-variant rounded-xl p-4 bg-surface-container-lowest animate-fade-in-down">
<div className="flex items-center justify-between mb-3 border-b border-neutral-100 pb-2">
<span className="font-sans font-bold text-sm text-on-surface flex items-center gap-2">
<Store className="w-4 h-4 text-primary" /> MSME (Udhyog Aadhar)
</span>
<button
type="button"
onClick={() => toggleDoc('Udhyog Aadhar')}
className="text-error hover:text-red-700 transition-colors p-0.5 rounded-full hover:bg-neutral-100"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="flex flex-col gap-3">
<input
type="text"
placeholder="Registration Number"
value={docNumbers['Udhyog Aadhar']}
onChange={(e) => handleDocNumberChange('Udhyog Aadhar', e.target.value)}
className="h-[46px] px-3.5 rounded-lg border border-outline bg-surface-container-lowest text-on-surface font-sans text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary placeholder:text-outline-variant"
/>
{uploadedDocs['Udhyog Aadhar'] ? (
<div className="flex items-center justify-between p-2 px-3 border border-green-200 bg-green-50/50 rounded-lg text-xs font-semibold text-green-800">
<span className="truncate flex items-center gap-1.5">
<Check className="w-4 h-4 text-green-700 stroke-[3]" />
{uploadedDocs['Udhyog Aadhar']}
</span>
<button type="button" onClick={() => clearDocUpload('Udhyog Aadhar')} className="text-red-500 hover:text-red-700 font-bold ml-2">Remove</button>
</div>
) : (
<button
type="button"
onClick={() => handleMockUpload('Udhyog Aadhar')}
className="flex items-center justify-center gap-2 h-11 w-full border-2 border-dashed border-secondary hover:bg-secondary/5 text-secondary rounded-lg font-sans font-bold text-xs transition-colors cursor-pointer"
>
<FileText className="w-4 h-4" />
Upload MSME Certificate
</button>
)}
</div>
</div>
)}
{/* PAN Details Container */}
{selectedDocs.includes('PAN') && (
<div className="border border-outline-variant rounded-xl p-4 bg-surface-container-lowest animate-fade-in-down">
<div className="flex items-center justify-between mb-3 border-b border-neutral-100 pb-2">
<span className="font-sans font-bold text-sm text-on-surface flex items-center gap-2">
<CreditCard className="w-4 h-4 text-primary" /> PAN Card Details
</span>
<button
type="button"
onClick={() => toggleDoc('PAN')}
className="text-error hover:text-red-700 transition-colors p-0.5 rounded-full hover:bg-neutral-100"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="flex flex-col gap-3">
<input
type="text"
maxLength={10}
placeholder="10-digit PAN Number"
value={docNumbers.PAN}
onChange={(e) => handleDocNumberChange('PAN', e.target.value.toUpperCase())}
className="h-[46px] px-3.5 rounded-lg border border-outline bg-surface-container-lowest text-on-surface font-sans text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary placeholder:text-outline-variant uppercase"
/>
{uploadedDocs.PAN ? (
<div className="flex items-center justify-between p-2 px-3 border border-green-200 bg-green-50/50 rounded-lg text-xs font-semibold text-green-800">
<span className="truncate flex items-center gap-1.5">
<Check className="w-4 h-4 text-green-700 stroke-[3]" />
{uploadedDocs.PAN}
</span>
<button type="button" onClick={() => clearDocUpload('PAN')} className="text-red-500 hover:text-red-700 font-bold ml-2">Remove</button>
</div>
) : (
<button
type="button"
onClick={() => handleMockUpload('PAN')}
className="flex items-center justify-center gap-2 h-11 w-full border-2 border-dashed border-secondary hover:bg-secondary/5 text-secondary rounded-lg font-sans font-bold text-xs transition-colors cursor-pointer"
>
<Camera className="w-4 h-4" />
Upload PAN Card Photo
</button>
)}
</div>
</div>
)}
</div>
</div>
{/* Input 6: Shop Facade Photo Upload Area */}
<div className="space-y-2 text-left pt-1">
<label className="text-xs font-bold text-on-surface uppercase tracking-wide block">
Shop Facade Photo <span className="text-error">*</span>
</label>
<div className="relative group cursor-pointer w-full">
<input
type="file"
accept="image/*"
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
setFacadePhotoUploaded(true);
triggerToast('📸 Front view photo captured successfully!');
}
}}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
<div className={`w-full aspect-video rounded-xl border-2 border-dashed transition-all duration-200 flex flex-col items-center justify-center gap-2 p-4 ${
facadePhotoUploaded
? 'border-green-600 bg-green-50/10'
: 'border-outline-variant bg-surface-container-lowest hover:bg-neutral-50/50'
}`}>
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
facadePhotoUploaded ? 'bg-green-100 text-green-700' : 'bg-surface-variant text-on-surface-variant'
}`}>
{facadePhotoUploaded ? <Check className="w-5 h-5 stroke-[3]" /> : <Camera className="w-5 h-5" />}
</div>
<div className="text-center">
<span className="block font-sans font-bold text-[14px] text-on-surface">
{facadePhotoUploaded ? 'Front View Uploaded' : 'Upload front view'}
</span>
<span className="block text-[12px] text-on-surface-variant mt-0.5">
{facadePhotoUploaded ? 'Click or Drag to replace photo' : 'Clear photo showing shop board'}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
{/* Pinned Action zone */}
<div className="absolute bottom-0 left-0 w-full bg-white border-t border-surface-container-high shadow-[0_-4px_16px_rgba(0,0,0,0.05)] px-margin-page pt-4 pb-6 z-30">
<div className="max-w-md mx-auto">
<button
onClick={handleSubmit}
disabled={!isFormValid}
className={`w-full h-[54px] font-display font-bold text-base rounded-2xl shadow-md flex items-center justify-center gap-2 transition-all active:scale-[0.98] ${
isFormValid
? 'bg-primary hover:bg-primary-container text-white shadow-[0_4px_16px_rgba(134,0,13,0.22)]'
: 'bg-surface-container-highest text-on-surface-variant/40 cursor-not-allowed shadow-none'
}`}
id="btn-business-continue"
>
<span>Continue to Verification</span>
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,178 @@
import React, { useState } from 'react';
import { ArrowLeft, Settings, Scan, ReceiptText, Package, Check } from 'lucide-react';
import { OnboardingState, BusinessFeatures } from '../types';
interface CustomizeSetupProps {
onBack: () => void;
onSkip: () => void;
onContinue: (features: BusinessFeatures) => void;
state: OnboardingState;
}
export default function CustomizeSetupScreen({ onBack, onSkip, onContinue, state }: CustomizeSetupProps) {
const [barcode, setBarcode] = useState(state.features?.barcodeScanner ?? false);
const [kot, setKot] = useState(state.features?.kotManagement ?? false);
const [inventory, setInventory] = useState(state.features?.inventoryAlerts ?? true);
const handleSubmit = () => {
onContinue({
barcodeScanner: barcode,
kotManagement: kot,
inventoryAlerts: inventory
});
};
return (
<div className="flex-1 flex flex-col h-full bg-background relative select-none animate-fade-in-up">
{/* Top Header Navigation */}
<header className="flex items-center justify-between px-margin-page h-[56px] border-b border-surface-container-high/60 bg-white/80 backdrop-blur z-20">
<div className="flex items-center gap-2 text-primary">
<Settings className="w-5 h-5 text-primary stroke-[2.3] animate-[spin_5s_linear_infinite]" />
<span className="font-display font-extrabold text-[16px] text-primary tracking-tight">PozoApp</span>
</div>
<button
onClick={onSkip}
className="font-sans font-bold text-xs text-on-surface-variant/80 hover:text-primary transition-colors cursor-pointer"
type="button"
id="btn-customize-skip"
>
Skip for Now
</button>
</header>
{/* Progress sliding bar: 80% filled (4 of 5 segments active) */}
<div className="w-full h-1.5 bg-surface-container-highest flex">
<div className="h-full bg-primary flex-1"></div>
<div className="h-full bg-primary flex-1"></div>
<div className="h-full bg-primary flex-1"></div>
<div className="h-full bg-primary flex-1"></div>
<div className="h-full bg-surface-container-highest flex-1"></div>
</div>
<div className="px-margin-page py-2 bg-slate-50 border-b border-surface-container-high/40 flex items-center justify-between text-[11px] font-sans font-bold text-on-surface-variant">
<span>Step 4 of 5</span>
<span className="text-primary italic font-display">Smart Setup</span>
</div>
{/* Toggles Container stream */}
<main className="flex-1 overflow-y-auto no-scrollbar pt-5 pb-32 px-margin-page bg-pattern flex flex-col justify-between">
<div className="space-y-6">
{/* Header titles */}
<div className="space-y-1 text-left">
<h2 className="font-display font-extrabold text-[22px] text-on-surface leading-tight">Customize Your Setup</h2>
<p className="font-sans text-[13px] text-on-surface-variant leading-relaxed">
Turn on the features you need for your shop. You can always change these later in settings.
</p>
</div>
{/* List of custom feature toggle cards */}
<div className="space-y-3.5">
{/* Card 1: Barcode Scanner */}
<div className="bg-white rounded-2xl p-4 sm:p-5 shadow-sm border border-surface-container-high/85 flex items-start justify-between gap-4">
<div className="flex items-start gap-4 flex-grow text-left">
<div className="w-11 h-11 rounded-full bg-slate-100 flex items-center justify-center shrink-0 text-on-surface-variant">
<Scan className="w-5.5 h-5.5 text-on-surface-variant" />
</div>
<div>
<h3 className="font-sans font-extrabold text-[15px] sm:text-base text-on-surface mb-0.5">Barcode Scanner</h3>
<p className="font-sans text-[11px] sm:text-xs text-on-surface-variant leading-normal">Do you use a Barcode Scanner?</p>
</div>
</div>
{/* Custom interactive Switch element */}
<button
type="button"
onClick={() => setBarcode(!barcode)}
className={`relative inline-flex h-6.5 w-12 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
barcode ? 'bg-primary' : 'bg-surface-container-highest'
}`}
id="toggle-barcode"
>
<span className={`pointer-events-none inline-block h-5.5 w-5.5 transform rounded-full bg-white shadow-md ring-0 transition duration-200 ease-in-out ${
barcode ? 'translate-x-[22px]' : 'translate-x-0.5'
}`}></span>
</button>
</div>
{/* Card 2: KOT Management */}
<div className="bg-white rounded-2xl p-4 sm:p-5 shadow-sm border border-surface-container-high/85 flex items-start justify-between gap-4">
<div className="flex items-start gap-4 flex-grow text-left">
<div className="w-11 h-11 rounded-full bg-slate-100 flex items-center justify-center shrink-0 text-on-surface-variant">
<ReceiptText className="w-5.5 h-5.5 text-on-surface-variant" />
</div>
<div>
<h3 className="font-sans font-extrabold text-[15px] sm:text-base text-on-surface mb-0.5">KOT Management</h3>
<p className="font-sans text-[11px] sm:text-xs text-on-surface-variant leading-normal">Do you need Kitchen Order Ticket management?</p>
</div>
</div>
{/* Custom Switch element */}
<button
type="button"
onClick={() => setKot(!kot)}
className={`relative inline-flex h-6.5 w-12 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
kot ? 'bg-primary' : 'bg-surface-container-highest'
}`}
id="toggle-kot"
>
<span className={`pointer-events-none inline-block h-5.5 w-5.5 transform rounded-full bg-white shadow-md ring-0 transition duration-200 ease-in-out ${
kot ? 'translate-x-[22px]' : 'translate-x-0.5'
}`}></span>
</button>
</div>
{/* Card 3: Inventory Alerts */}
<div className="bg-white rounded-2xl p-4 sm:p-5 shadow-sm border border-surface-container-high/85 flex items-start justify-between gap-4">
<div className="flex items-start gap-4 flex-grow text-left">
<div className="w-11 h-11 rounded-full bg-slate-100 flex items-center justify-center shrink-0 text-on-surface-variant">
<Package className="w-5.5 h-5.5 text-on-surface-variant" />
</div>
<div>
<h3 className="font-sans font-extrabold text-[15px] sm:text-base text-on-surface mb-0.5">Inventory Alerts</h3>
<p className="font-sans text-[11px] sm:text-xs text-on-surface-variant leading-normal">Get notified when stock is running low.</p>
</div>
</div>
{/* Custom Switch element (pre-checked, with a mini check inside handle) */}
<button
type="button"
onClick={() => setInventory(!inventory)}
className={`relative inline-flex h-6.5 w-12 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
inventory ? 'bg-primary' : 'bg-surface-container-highest'
}`}
id="toggle-inventory"
>
<span className={`pointer-events-none h-5.5 w-5.5 transform rounded-full bg-white shadow-md ring-0 transition duration-200 ease-in-out flex items-center justify-center ${
inventory ? 'translate-x-[22px]' : 'translate-x-0.5'
}`}>
{inventory && <Check className="w-3.5 h-3.5 text-primary stroke-[3.5]" />}
</span>
</button>
</div>
</div>
</div>
</main>
{/* Pinned Bottom action */}
<div className="absolute bottom-0 left-0 w-full bg-white border-t border-surface-container-high shadow-[0_-4px_16px_rgba(0,0,0,0.05)] px-margin-page pt-4 pb-6 z-30">
<div className="max-w-md mx-auto flex gap-3.5">
<button
onClick={onBack}
className="w-14 h-[54px] border-2 border-surface-container-highest text-on-surface-variant bg-white rounded-2xl flex items-center justify-center shadow-sm hover:bg-neutral-50 active:scale-95 transition-all shrink-0"
aria-label="Back"
>
<ArrowLeft className="w-5 h-5 text-on-surface-variant stroke-[2.3]" />
</button>
<button
onClick={handleSubmit}
className="flex-1 h-[54px] bg-primary hover:bg-primary-container text-white font-display font-bold text-base rounded-2xl shadow-[0_4px_16px_rgba(175,16,26,0.25)] flex items-center justify-center gap-2 transition-all active:scale-[0.98]"
id="btn-customize-continue"
>
<span>Save & Continue</span>
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,144 @@
import React, { useState, useEffect } from 'react';
import { Wifi, Battery, Signal, RefreshCw, Smartphone, Monitor, Info } from 'lucide-react';
interface DeviceFrameProps {
children: React.ReactNode;
onReset: () => void;
className?: string;
}
export default function DeviceFrame({ children, onReset, className = '' }: DeviceFrameProps) {
const [isFullscreen, setIsFullscreen] = useState(false);
const [time, setTime] = useState('12:30');
useEffect(() => {
// Standard system clock updates in high fidelity status bar
const updateTime = () => {
const now = new Date();
let hours = now.getHours();
const minutes = now.getMinutes().toString().padStart(2, '0');
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12 || 12;
setTime(`${hours}:${minutes} ${ampm}`);
};
updateTime();
const interval = setInterval(updateTime, 1000 * 30);
return () => clearInterval(interval);
}, []);
return (
<div className={`min-h-screen bg-neutral-900 w-full flex flex-col items-center justify-center p-2 sm:p-4 text-white overflow-y-auto ${className}`} id="device-root">
{/* Simulation HUD bar */}
<div className="w-full max-w-5xl mb-4 p-3 bg-neutral-800/80 backdrop-blur rounded-2xl flex flex-wrap items-center justify-between gap-3 shadow-md border border-neutral-700 z-50">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-brand-red flex items-center justify-center shadow-md animate-pulse">
<span className="text-white font-bold font-display text-sm">P</span>
</div>
<div>
<h4 className="font-display font-bold text-sm tracking-tight text-white flex items-center gap-1.5">
PozoApp Live Simulator
<span className="text-[10px] bg-emerald-500/20 text-emerald-400 border border-emerald-500/20 px-1.5 py-0.5 rounded-full font-sans uppercase">Interactive Demo</span>
</h4>
<p className="text-[11px] text-neutral-400 font-sans">Realistic state synchronization between configuration pages</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Layout switches */}
<button
onClick={() => setIsFullscreen(false)}
className={`px-3 py-1.5 rounded-lg flex items-center gap-1.5 text-xs font-medium transition-all ${
!isFullscreen
? 'bg-brand-red text-white shadow-sm'
: 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'
}`}
title="Mobile device mockup aspect ratio"
id="btn-layout-mobile"
>
<Smartphone className="w-3.5 h-3.5" />
<span>Mobile View</span>
</button>
<button
onClick={() => setIsFullscreen(true)}
className={`px-3 py-1.5 rounded-lg flex items-center gap-1.5 text-xs font-medium transition-all ${
isFullscreen
? 'bg-brand-red text-white shadow-sm'
: 'bg-neutral-700 text-neutral-300 hover:bg-neutral-600'
}`}
title="Full screen edge-to-edge view"
id="btn-layout-full"
>
<Monitor className="w-3.5 h-3.5" />
<span>Fluid Web</span>
</button>
<div className="w-[1px] h-6 bg-neutral-700 mx-1"></div>
{/* Reset Demo State button */}
<button
onClick={onReset}
className="p-1.5 rounded-lg bg-neutral-700 hover:bg-neutral-600 text-neutral-300 transition-colors flex items-center gap-1.5 text-xs font-medium"
title="Clear state and restart onboarding"
id="btn-reset-simulator"
>
<RefreshCw className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Reset Form</span>
</button>
</div>
</div>
{isFullscreen ? (
/* Full-screen Fluid responsive container */
<div className="w-full max-w-3xl min-h-[85vh] bg-background text-on-surface rounded-3xl shadow-2xl relative overflow-hidden transition-all duration-300 border border-neutral-700 flex flex-col animate-fade-in-up">
{children}
</div>
) : (
/* Physical Mobile Smartphone frame mockup */
<div className="relative w-full max-w-[420px] aspect-[9/19.5] min-h-[780px] bg-black rounded-[52px] shadow-[0_25px_60px_rgba(0,0,0,0.8)] border-[9px] border-neutral-800 p-3 hover:shadow-[0_25px_70px_rgba(175,16,26,0.15)] transition-all duration-500 overflow-hidden flex flex-col select-none animate-fade-in-up">
{/* Speaker, camera notch, face ID pill */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 h-[30px] w-[140px] bg-black rounded-b-2xl z-50 flex items-center justify-center gap-2 px-4 shadow-[inset_0_2px_4px_rgba(255,255,255,0.1)]">
<div className="w-12 h-1 bg-neutral-800 rounded-full"></div>
<div className="w-3.5 h-3.5 bg-neutral-900 rounded-full border border-neutral-800 flex-shrink-0 relative overflow-hidden">
<div className="absolute top-0.5 left-0.5 w-1.5 h-1.5 bg-blue-900 rounded-full opacity-60"></div>
</div>
</div>
/* Inner Dynamic viewport */
<div className="w-full h-full bg-background text-on-surface rounded-[42px] overflow-hidden relative flex flex-col">
{/* Standard status bar */}
<div className="h-10 pt-4 px-6 flex items-center justify-between text-on-surface text-xs font-medium z-40 bg-transparent select-none">
<span className="font-display font-bold tracking-tight text-[11px] scale-110 ml-1">{time.split(' ')[0]}</span>
<div className="flex items-center gap-1.5">
<Signal className="w-3.5 h-3.5 stroke-[2.5]" />
<span className="text-[9px] uppercase tracking-wider font-semibold">5G</span>
<Wifi className="w-3.5 h-3.5 stroke-[2.5]" />
<div className="flex items-center gap-0.5">
<span className="text-[10px] scale-90">100%</span>
<Battery className="w-4.5 h-4.5 stroke-[1.8] fill-on-surface" />
</div>
</div>
</div>
{/* Simulated Content stream wrapper */}
<div className="flex-1 w-full overflow-hidden flex flex-col relative">
{children}
</div>
{/* Virtual bottom navigation slider bar (iOS/Android home stroke) */}
<div className="absolute bottom-1.5 left-1/2 -translate-x-1/2 w-32 h-1 bg-on-surface/40 rounded-full z-50 pointer-events-none"></div>
</div>
</div>
)}
{/* Embedded documentation card inside preview */}
<div className="w-full max-w-md mt-6 p-4 bg-neutral-800/60 backdrop-blur rounded-2xl border border-neutral-700 text-neutral-300 text-center gap-2 flex items-center text-xs justify-center font-sans">
<Info className="w-4 h-4 text-brand-red flex-shrink-0" />
<p className="leading-relaxed">
Flutter Dart classes are also auto-generated inside this workspace at <code className="bg-neutral-900 px-1 py-0.5 rounded font-mono text-brand-red">/flutter_app/</code> for cross-platform Android, iOS and Web compilations.
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,213 @@
import React, { useState } from 'react';
import { ArrowLeft, Lock, MessageSquare, ShieldCheck, Zap, Shield } from 'lucide-react';
import { OnboardingState } from '../types';
interface MobileLoginProps {
onBack: () => void;
onContinue: (mobile: string) => void;
state: OnboardingState;
}
export default function MobileLoginScreen({ onBack, onContinue, state }: MobileLoginProps) {
const [mobile, setMobile] = useState(state.mobileNumber || '');
const [error, setError] = useState('');
// Validate Indian Phone standard format
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D/g, ''); // Numeric only
if (value.length <= 10) {
setMobile(value);
if (value.length === 10) {
setError('');
}
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (mobile.length !== 10) {
setError('Please enter a valid 10-digit mobile number');
return;
}
setError('');
onContinue(mobile);
};
const handleTruecallerVerify = () => {
setMobile('9876543210');
setError('');
// Simulate instant verification redirect/transition
setTimeout(() => {
onContinue('9876543210');
}, 500);
};
const isValid = mobile.length === 10;
return (
<div className="flex-1 flex flex-col h-full bg-[#f9f9f9] relative select-none animate-fade-in-up">
{/* Top Application Header Bar */}
<header className="flex items-center justify-between px-margin-page h-[56px] bg-transparent border-b border-gray-100 z-20">
<button
onClick={onBack}
className="p-1 px-1.5 -ml-1 rounded-full text-on-surface-variant hover:bg-gray-100 transition-colors flex items-center justify-center"
aria-label="Back"
id="btn-login-back"
>
<ArrowLeft className="w-5 h-5 text-[#af101a]" strokeWidth={2.5} />
</button>
<span className="font-display font-extrabold text-[17px] text-[#af101a] tracking-tight">PozoApp</span>
<div className="w-8"></div> {/* Spacer to keep title left-ish / aligned */}
</header>
{/* Primary scrollable Form view */}
<form onSubmit={handleSubmit} className="flex-1 flex flex-col px-margin-page pt-6 pb-40 overflow-y-auto no-scrollbar">
{/* 1. Progress Bar Step Label & Bar */}
<div className="mb-6">
<div className="flex justify-between items-center mb-2">
<span className="font-sans text-xs font-semibold text-gray-500">Step 2 of 5</span>
<span className="font-display text-xs font-semibold text-[#af101a]">Mobile Login</span>
</div>
<div className="flex gap-1.5 h-1.5 w-full">
<div className="flex-1 bg-[#af101a] rounded-full"></div>
<div className="flex-1 bg-[#af101a] rounded-full"></div>
<div className="flex-1 bg-[#e2e2e2] rounded-full"></div>
<div className="flex-1 bg-[#e2e2e2] rounded-full"></div>
<div className="flex-1 bg-[#e2e2e2] rounded-full"></div>
</div>
</div>
{/* Welcome titles */}
<div className="space-y-2 mb-6 text-left">
<h2 className="font-display font-extrabold text-[24px] text-on-surface leading-tight text-gray-900">Welcome Back</h2>
<p className="font-sans text-[13px] text-gray-600 leading-relaxed">
Enter your mobile number to sign in or create a new account.
</p>
{/* 6. Top Badge: Green "Secure OTP Verification" chip with lock icon */}
<div className="inline-flex items-center gap-1.5 bg-[#daffd1] text-[#016619] px-3 py-1.5 rounded-full border border-[#016619]/20 mt-1">
<Lock className="w-3.5 h-3.5" strokeWidth={2.5} />
<span className="font-sans text-[11px] font-bold">Secure OTP Verification</span>
</div>
</div>
{/* 2. Truecaller Card */}
<div className="mb-6 p-6 bg-[#f3f3f3] rounded-[16px] border border-[#e4beba]/50 shadow-sm flex flex-col">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-[#005faf] rounded-full flex items-center justify-center text-white shrink-0">
<Shield className="w-5 h-5 fill-white text-[#005faf]" strokeWidth={2.5} />
</div>
<div className="flex flex-col text-left">
<span className="font-sans text-xs text-gray-500 leading-none mb-1">Verify instantly with</span>
<span className="font-display text-lg font-bold text-black leading-tight">Truecaller</span>
</div>
</div>
<div className="flex flex-col items-center justify-center py-2 text-center">
<span className="font-display text-2xl font-extrabold text-black mb-4">+91 98765 43210</span>
<button
type="button"
onClick={handleTruecallerVerify}
className="w-full bg-[#005faf] text-white font-sans font-bold text-base rounded-[8px] h-[56px] flex items-center justify-center gap-2 hover:bg-[#004786] transition-all duration-200 active:scale-95 shadow-sm"
>
<Zap className="w-5 h-5 fill-white text-white" />
<span>1-Tap Verification</span>
</button>
<p className="mt-3 font-sans text-xs text-gray-500 px-2 line-clamp-2 leading-relaxed">
No OTP required. Your phone will be verified automatically.
</p>
</div>
</div>
{/* 3. Divider: "Or enter mobile number manually" */}
<div className="flex items-center gap-4 mb-6">
<div className="flex-grow h-[1px] bg-[#e4beba]"></div>
<span className="font-sans text-[11px] font-medium text-gray-500 whitespace-nowrap">
Or enter mobile number manually
</span>
<div className="flex-grow h-[1px] bg-[#e4beba]"></div>
</div>
{/* 4. Mobile Number input field */}
<div className="space-y-2 text-left">
<label htmlFor="input-mobile-number" className="block text-xs font-bold text-gray-700 tracking-wide">
Mobile Number
</label>
<div className={`flex items-center bg-white border ${
error ? 'border-error ring-1 ring-error' : 'border-[#e4beba] focus-within:border-[#af101a] focus-within:ring-1 focus-within:ring-[#af101a]'
} rounded-[8px] overflow-hidden h-[56px] shadow-sm transition-all duration-200`}>
{/* Country flag box */}
<div className="flex items-center justify-center px-4 bg-white h-full border-r border-[#e4beba] font-sans font-bold text-base text-gray-800 gap-2 select-none shrink-0">
<img
src="https://lh3.googleusercontent.com/aida-public/AB6AXuDvDQdmsq_37l0Tr6OYeOdB-DDEsA2jtYFomz8KjRVG5afnBqc4J7WjAPW9u-4c00ATBQ3qb1HaKpbcaQ3sTFvZMU9dHK2Vhf0W5uHH0c3SndsYrXXPkI5J8hUKl5Y8xwUV8HhSqXS2C3gxkHQ7CE5Eb36ZjW2Ky7h4AHpQvtOrtv3SWdWu90uFGnJFRoIFXFUnlpAfAAib0MbtapQTjcwNjcYpLW3_rVy6Y3KrJbQ3tLYstbfBXUudtYXkou89Y_1Xp6RNZUJxEhGU"
alt="India Flag"
className="w-5 h-auto rounded-[2px]"
/>
<span className="text-gray-800 font-bold text-base">+91</span>
</div>
{/* Telephone input box */}
<input
type="tel"
pattern="[0-9]*"
inputMode="numeric"
maxLength={10}
id="input-mobile-number"
placeholder="00000 00000"
value={mobile}
onChange={handleChange}
className="flex-grow h-full bg-transparent border-none text-gray-900 font-sans font-bold text-lg px-4 outline-none focus:ring-0 placeholder:text-gray-300"
autoFocus
/>
</div>
{error && (
<p className="text-xs text-red-600 font-medium pl-1 animate-slide-left">{error}</p>
)}
{/* Quick preset hint helper to expedite testing */}
<div className="p-3.5 bg-gray-50 rounded-lg border border-gray-100 mt-4">
<p className="text-[11px] text-gray-500 font-sans flex items-center gap-1.5 leading-relaxed">
<ShieldCheck className="w-4 h-4 text-emerald-600 shrink-0" />
<span>You can enter any 10 digits (e.g. <span className="font-bold font-mono">9876543210</span>) to simulate. An automated SMS OTP will trigger next.</span>
</p>
</div>
</div>
</form>
{/* 5. Sticky Bottom Actions zone */}
<div className="absolute bottom-0 left-0 w-full bg-white border-t border-[#e4beba] shadow-[0_-4px_16px_rgba(0,0,0,0.05)] px-margin-page pt-4 pb-6 z-35">
<div className="max-w-md mx-auto space-y-3">
{/* Full width red "Send OTP" button */}
<button
onClick={handleSubmit}
disabled={!isValid}
className={`w-full h-[56px] font-sans font-bold text-base rounded-[8px] shadow-sm flex items-center justify-center gap-2 transition-all active:scale-[0.98] ${
isValid
? 'bg-[#af101a] hover:bg-[#86000d] text-white shadow-[0_4px_16px_rgba(175,16,26,0.2)]'
: 'bg-gray-200 text-gray-400 cursor-not-allowed shadow-none'
}`}
id="btn-login-submit"
>
<span>Send OTP</span>
</button>
{/* Outline style red "Help / Talk to Support" button */}
<button
type="button"
className="w-full h-[56px] bg-white text-[#af101a] border-2 border-[#af101a] font-sans font-bold text-sm rounded-[8px] flex items-center justify-center gap-2 hover:bg-gray-50 transition-all active:scale-95"
id="btn-login-support"
onClick={() => alert("Connecting with Pozo Support... Please wait.")}
>
<MessageSquare className="w-4 h-4 text-[#af101a] fill-[#af101a]/10" strokeWidth={2.3} />
<span>Help / Talk to Support</span>
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,298 @@
import React, { useState, useEffect, useRef } from 'react';
import { ArrowLeft, Clock, MessageCircle, HelpCircle, Save, ArrowRight, MessageSquareCode } from 'lucide-react';
import { OnboardingState } from '../types';
interface OtpVerificationProps {
onBack: () => void;
onContinue: (otp: string[]) => void;
state: OnboardingState;
}
export default function OtpVerificationScreen({ onBack, onContinue, state }: OtpVerificationProps) {
const [digits, setDigits] = useState<string[]>(['', '', '', '', '', '']);
const [timeLeft, setTimeLeft] = useState(45);
const [showNotification, setShowNotification] = useState(false);
const [focusIndex, setFocusIndex] = useState(0);
const inputRefs = useRef<HTMLInputElement[]>([]);
// Real-time OTP count-down clock simulation
useEffect(() => {
if (timeLeft <= 0) return;
const timer = setInterval(() => {
setTimeLeft((prev) => prev - 1);
}, 1000);
return () => clearInterval(timer);
}, [timeLeft]);
// Push SMS banner trigger after 2 seconds for high fidelity demo
useEffect(() => {
const pushTimer = setTimeout(() => {
setShowNotification(true);
}, 2000);
return () => clearTimeout(pushTimer);
}, []);
const handleDigitChange = (index: number, value: string) => {
const val = value.replace(/\D/g, '').slice(-1); // Only allow last digit written
const updated = [...digits];
updated[index] = val;
setDigits(updated);
// Auto focus next box
if (val && index < 5) {
setFocusIndex(index + 1);
inputRefs.current[index + 1]?.focus();
}
};
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Backspace' && !digits[index] && index > 0) {
const updated = [...digits];
updated[index - 1] = '';
setDigits(updated);
setFocusIndex(index - 1);
inputRefs.current[index - 1]?.focus();
}
};
const triggerResend = () => {
setTimeLeft(45);
};
// Automated typing simulator on SMS Click
const autoDetectOtpAction = () => {
const sampleCode = ['9', '4', '3', '7', '1', '5'];
// Type out digits dynamically for user delight
sampleCode.forEach((char, idx) => {
setTimeout(() => {
setDigits((prev) => {
const next = [...prev];
next[idx] = char;
return next;
});
setFocusIndex(idx === 5 ? 5 : idx + 1);
if (idx === 5) {
// Finish and carry forward
setTimeout(() => {
onContinue(sampleCode);
}, 600);
}
}, idx * 150);
});
setShowNotification(false);
};
const formattedTime = `00:${timeLeft.toString().padStart(2, '0')}`;
const isOtpComplete = digits.every(digit => digit !== '');
const handleManualVerify = () => {
if (isOtpComplete) {
onContinue(digits);
} else {
// Just bypass for smoother testing with default OTP
onContinue(['5', '5', '5', '5', '5', '5']);
}
};
const displayMobile = state.mobileNumber
? `+91 ${state.mobileNumber.slice(0, 5)} ${state.mobileNumber.slice(5)}`
: '+91 98765 43210';
return (
<div className="flex-1 flex flex-col h-full bg-background relative select-none animate-fade-in-up">
{/* Toast SMS Notification Pop Banner (Simulating Auto-OTP Trigger) */}
{showNotification && (
<div
onClick={autoDetectOtpAction}
className="absolute top-2 left-3 right-3 bg-neutral-900/95 backdrop-blur border border-neutral-700/60 p-3 rounded-2xl flex items-start gap-2.5 shadow-2xl z-50 text-left animate-bounce cursor-pointer hover:border-primary transition-all"
id="toast-sms-notification"
>
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center shrink-0 shadow-sm">
<MessageSquareCode className="w-4 h-4 text-white" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="text-[11px] font-bold text-white font-sans tracking-wide">Messages now</span>
<span className="text-[9px] bg-primary/20 text-primary-fixed-dim px-1.5 py-0.5 rounded font-bold uppercase font-sans">Simulated SMS</span>
</div>
<p className="text-xs font-semibold text-white mt-0.5">PozoApp: Your OTP verification check code matches <span className="text-primary-fixed-dim font-bold font-mono text-sm underline">943715</span>.</p>
<p className="text-[10px] text-emerald-400 font-bold mt-1.5 flex items-center gap-1">
<span> Click to auto-detect & compile code</span>
</p>
</div>
</div>
)}
{/* Top App bar */}
<header className="flex items-center justify-between px-margin-page h-[56px] border-b border-surface-container-high/60 bg-white/80 backdrop-blur z-20">
<button
onClick={onBack}
className="p-1 px-1.5 -ml-1 rounded-full text-on-surface-variant hover:bg-surface-container-low transition-colors"
aria-label="Back"
id="btn-otp-back"
>
<ArrowLeft className="w-5 h-5 text-primary" strokeWidth={2.5} />
</button>
<span className="font-display font-extrabold text-[17px] text-primary tracking-tight">PozoApp</span>
<span className="text-on-surface-variant font-sans text-[10px] font-semibold">Step 2.5 of 5</span>
</header>
{/* Progress sliding bar with interactive 2.5/5 index state */}
<div className="w-full h-1.5 bg-surface-container-highest flex">
<div className="h-full bg-primary flex-1"></div>
<div className="h-full bg-primary flex-1"></div>
<div className="h-full bg-gradient-to-r from-primary to-surface-container-highest flex-1 relative">
<div className="absolute left-0 w-1/2 h-full bg-primary"></div>
</div>
<div className="h-full bg-surface-container-highest flex-1"></div>
<div className="h-full bg-surface-container-highest flex-1"></div>
</div>
{/* Core View panel */}
<div className="flex-1 overflow-y-auto no-scrollbar pt-6 pb-28 px-margin-page bg-pattern flex flex-col justify-between">
{/* Verification pulse badge illustration */}
<div className="flex flex-col items-center text-center">
<div className="w-24 h-24 bg-surface-container-high rounded-full flex items-center justify-center mb-4 relative">
<div className="w-18 h-18 bg-white rounded-2xl shadow-md flex items-center justify-center z-10">
<span className="material-symbols-outlined text-primary text-4xl" style={{ fontVariationSettings: "'FILL' 1" }}>sms</span>
</div>
{/* Pulsing rings */}
<div className="absolute inset-0 bg-primary/10 rounded-full animate-ping opacity-75"></div>
</div>
<h2 className="text-xl font-display font-extrabold text-on-surface leading-tight mb-1">Verifying your number</h2>
<p className="text-xs sm:text-sm text-on-surface-variant font-sans max-w-[280px]">
We've sent a 6-digit code to <span className="font-bold text-on-surface">{displayMobile}</span>
<button
onClick={onBack}
className="text-primary font-bold ml-1.5 hover:underline transition-all active:scale-95 text-xs inline-block"
type="button"
id="link-otp-edit"
>
Edit
</button>
</p>
</div>
{/* OTP Input block */}
<div className="my-6">
<div className="flex justify-between gap-1.5 sm:gap-2 mb-6">
{digits.map((digit, index) => (
<div
key={index}
onClick={() => {
setFocusIndex(index);
inputRefs.current[index]?.focus();
}}
className={`w-11 sm:w-12 h-14 bg-white border-b-2 ${
digit
? 'border-primary'
: focusIndex === index
? 'border-primary shadow-sm bg-primary/5'
: 'border-outline-variant'
} flex items-center justify-center rounded-t-lg transition-all duration-200 cursor-pointer shadow-sm relative`}
>
{digit ? (
<span className="font-sans font-extrabold text-lg text-on-surface">{digit}</span>
) : focusIndex === index ? (
<div className="w-[2px] h-6 bg-primary animate-[pulse_1s_infinite] rounded-full"></div>
) : null}
{/* Secret focus handler inputs */}
<input
ref={el => { if (el) inputRefs.current[index] = el; }}
type="text"
maxLength={1}
pattern="[0-9]*"
inputMode="numeric"
value={digit}
onChange={(e) => handleDigitChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
onFocus={() => setFocusIndex(index)}
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
/>
</div>
))}
</div>
{/* Loading loader alert box */}
<div className="flex items-center justify-center gap-2 py-3 bg-surface-container-low border border-surface-container-highest rounded-xl mb-6">
<div className="flex gap-1">
<div className="w-1.5 h-1.5 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-1.5 h-1.5 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
<div className="w-1.5 h-1.5 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.3s' }}></div>
</div>
<span className="text-[11px] font-bold text-on-surface-variant font-sans tracking-wide">Auto-detecting OTP...</span>
</div>
{/* Interactive actions row */}
<div className="flex flex-col items-center gap-3">
<div className="flex items-center gap-1.5">
<Clock className="w-4 h-4 text-on-surface-variant" />
<span className="text-xs text-on-surface-variant font-sans">
{timeLeft > 0 ? (
<>Resend code in <span className="font-bold text-on-surface font-mono">{formattedTime}</span></>
) : (
<button
onClick={triggerResend}
className="text-primary font-bold text-xs underline cursor-pointer active:scale-95"
type="button"
id="btn-otp-resend"
>
Resend Code via SMS
</button>
)}
</span>
</div>
{/* WhatsApp CTA */}
<button
type="button"
className="flex items-center gap-2 px-5 py-2.5 border border-outline-variant bg-white rounded-full hover:bg-neutral-50 transition-colors shadow-sm active:scale-95"
id="btn-otp-whatsapp"
>
{/* WhatsApp custom high contrast vector icon */}
<div className="w-4.5 h-4.5 bg-emerald-500 rounded-full flex items-center justify-center">
<MessageCircle className="w-3 h-3 text-white fill-white stroke-none" />
</div>
<span className="text-xs font-bold text-on-surface-variant font-sans">Resend via WhatsApp</span>
</button>
</div>
</div>
</div>
{/* Modern navigation dock with primary verifying triggers */}
<footer className="absolute bottom-0 left-0 right-0 w-full flex justify-around items-center h-20 px-3 bg-white border-t border-surface-container-high/80 shadow-lg z-30 rounded-t-2xl">
<button
type="button"
className="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary transition-colors p-2 shrink-0"
id="btn-otp-help"
>
<HelpCircle className="w-5 h-5 text-on-surface-variant stroke-[2.3]" />
<span className="text-[10px] font-bold font-sans mt-0.5">Help</span>
</button>
<button
type="button"
className="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary transition-colors p-2 shrink-0"
id="btn-otp-save"
>
<Save className="w-5 h-5 text-on-surface-variant stroke-[2.3]" />
<span className="text-[10px] font-bold font-sans mt-0.5">Save Draft</span>
</button>
{/* The Action triggering forward */}
<button
type="button"
onClick={handleManualVerify}
className="flex items-center justify-center gap-1 bg-primary text-white font-sans font-bold text-xs px-5 py-2.5 rounded-full transition-all active:scale-[0.97] hover:bg-primary-container shadow-md shrink-0"
id="btn-otp-verify-manually"
>
<span>Verify Manually</span>
<ArrowRight className="w-3.5 h-3.5 stroke-[2.5]" />
</button>
</footer>
</div>
);
}

View File

@ -0,0 +1,167 @@
import React, { useEffect, useState } from 'react';
import { Check, Store, Receipt, Sparkles } from 'lucide-react';
import { OnboardingState, BUSINESS_TYPES } from '../types';
interface SetupCompleteProps {
onContinue: () => void;
state: OnboardingState;
}
interface ConfettiPart {
id: number;
left: string;
size: string;
delay: string;
duration: string;
color: string;
shape: string;
}
export default function SetupCompleteScreen({ onContinue, state }: SetupCompleteProps) {
const [confetti, setConfetti] = useState<ConfettiPart[]>([]);
useEffect(() => {
// Generate simulated paper confetti arrays on mount
const colors = ['#af101a', '#54a0fe', '#298030', '#ffb3ac', '#ffdad6', '#a5c8ff', '#82db7e'];
const shapes = ['circle', 'square', 'triangle'];
const items = Array.from({ length: 45 }).map((_, i) => ({
id: i,
left: `${Math.random() * 100}vw`,
size: `${Math.random() * 8 + 6}px`,
delay: `${Math.random() * 3}s`,
duration: `${Math.random() * 2.5 + 2.5}s`,
color: colors[Math.floor(Math.random() * colors.length)],
shape: shapes[Math.floor(Math.random() * shapes.length)]
}));
setConfetti(items);
}, []);
const getBusinessTypeName = (typeId: string) => {
const matched = BUSINESS_TYPES.find(b => b.id === typeId);
if (matched) return matched.name;
return 'Retail / Grocery';
};
const businessTitle = state.businessName || 'Sharma General Store';
const typeDisplay = state.businessType ? getBusinessTypeName(state.businessType) : 'Retail / Grocery';
return (
<div className="flex-1 flex flex-col h-full bg-background relative overflow-hidden select-none bg-pattern animate-fade-in-up">
{/* Dynamic Cascading Confetti Layers */}
<div className="absolute inset-0 pointer-events-none z-0 overflow-hidden opacity-85">
{confetti.map((c) => (
<div
key={c.id}
className="absolute"
style={{
left: c.left,
top: '-20px',
width: c.size,
height: c.size,
backgroundColor: c.color,
borderRadius: c.shape === 'circle' ? '50%' : c.shape === 'triangle' ? '0 50% 50% 100%' : '2px',
animationName: 'fall',
animationDuration: c.duration,
animationDelay: c.delay,
animationTimingFunction: 'linear',
animationIterationCount: 'infinite',
transform: `rotate(${Math.random() * 350}deg)`
}}
/>
))}
</div>
{/* Embedded falling animation style injection */}
<style>{`
@keyframes fall {
0% {
transform: translateY(-20px) rotate(0deg);
opacity: 1;
}
90% {
opacity: 0.9;
}
100% {
transform: translateY(105vh) rotate(360deg);
opacity: 0;
}
}
`}</style>
{/* Top micro progress indicator (Completely active 100% completed) */}
<div className="w-full h-1.5 bg-primary relative z-10 transition-all duration-300"></div>
<div className="px-margin-page py-2.5 bg-slate-100 border-b border-surface-container-high/40 flex items-center justify-between text-[11px] font-sans font-bold text-on-surface-variant z-10">
<span>Step 5 of 5 Completed</span>
<span className="text-emerald-700 italic font-display flex items-center gap-1">
<Sparkles className="w-3 h-3 text-emerald-600 shrink-0" />
Onboarding Done
</span>
</div>
{/* Main Stream centered scroll wrapper */}
<main className="flex-grow w-full max-w-md mx-auto flex flex-col items-center justify-center px-margin-page pt-4 pb-32 relative z-10 overflow-y-auto no-scrollbar">
{/* Rounded double rings success check icon bubble */}
<div className="w-24 h-24 rounded-full bg-surface-container-lowest border-4 border-white shadow-[0_8px_24px_rgba(0,0,0,0.085)] flex items-center justify-center mb-6 animate-pop-in relative">
<div className="w-18 h-18 rounded-full bg-emerald-500 flex items-center justify-center animate-pulse">
<Check className="w-9 h-9 text-white stroke-[4]" />
</div>
{/* Echo ping ring */}
<div className="absolute inset-x-0 inset-y-0 border-2 border-emerald-500 rounded-full animate-ping opacity-25"></div>
</div>
{/* Celebrating message */}
<div className="text-center space-y-2.5 mb-8">
<h1 className="font-display font-extrabold text-[24px] sm:text-[27px] text-on-surface tracking-tight">
Setup Complete! 🎉
</h1>
<p className="font-sans text-[13px] sm:text-[14px] text-on-surface-variant leading-relaxed max-w-[270px] mx-auto">
Your billing dashboard is ready for action.
</p>
</div>
{/* Dynamic Glassmorphic Recap Card */}
<div className="w-full bg-white/90 backdrop-blur-md rounded-2xl p-5 sm:p-6 shadow-[0_4px_22px_rgba(0,0,0,0.06)] border border-surface-container-highest/85 flex flex-col gap-4 text-left transition-all duration-300 transform hover:translate-y-[-2px]">
{/* Section Row 1: Business name */}
<div className="flex items-start gap-4">
<div className="w-11 h-11 rounded-full bg-slate-100 flex items-center justify-center shrink-0">
<Store className="w-5.5 h-5.5 text-on-surface-variant stroke-[1.8]" />
</div>
<div className="space-y-0.5">
<p className="font-sans text-[10px] font-bold text-on-surface-variant/80 uppercase tracking-widest">Business Name</p>
<h4 className="font-display font-extrabold text-base sm:text-lg text-on-surface tracking-tight line-clamp-1">{businessTitle}</h4>
</div>
</div>
{/* Dividing border */}
<div className="h-[1px] bg-slate-100 w-full" />
{/* Section Row 2: Category */}
<div className="flex items-start gap-4">
<div className="w-11 h-11 rounded-full bg-slate-100 flex items-center justify-center shrink-0">
<span className="material-symbols-outlined text-on-surface-variant text-xl" style={{ fontVariationSettings: "'FILL' 0" }}>category</span>
</div>
<div className="space-y-0.5">
<p className="font-sans text-[10px] font-bold text-on-surface-variant/80 uppercase tracking-widest">Business Type</p>
<p className="font-sans font-bold text-sm sm:text-[15px] text-on-surface-variant">{typeDisplay}</p>
</div>
</div>
</div>
</main>
{/* Pinned Bottom final launcher button */}
<div className="absolute bottom-0 left-0 w-full bg-white border-t border-surface-container-high shadow-[0_-4px_16px_rgba(0,0,0,0.05)] px-margin-page pt-4 pb-6 z-30">
<div className="max-w-md mx-auto">
<button
onClick={onContinue}
className="w-full h-[54px] bg-primary hover:bg-primary-container text-white font-display font-bold text-base rounded-2xl shadow-[0_4px_16px_rgba(175,16,26,0.3)] flex items-center justify-center gap-2 transition-all active:scale-[0.98]"
id="btn-complete-create-bill"
>
<Receipt className="w-5 h-5" strokeWidth={2.3} />
<span>Create Your First Bill</span>
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,130 @@
import React from 'react';
import { ArrowRight, Check, BadgeCheck, FileText, Wallet, Percent, TrendingUp } from 'lucide-react';
import { OnboardingState } from '../types';
interface WelcomeScreenProps {
onContinue: () => void;
state: OnboardingState;
}
export default function WelcomeScreen({ onContinue }: WelcomeScreenProps) {
return (
<div className="flex-1 flex flex-col h-full bg-background relative overflow-hidden select-none bg-pattern animate-fade-in-up">
{/* Top micro progress indicator */}
<div className="w-full flex gap-1 px-margin-page pt-4 z-10">
<div className="h-1 flex-1 bg-primary rounded-full transition-all duration-300"></div>
<div className="h-1 flex-1 bg-surface-container-high rounded-full"></div>
<div className="h-1 flex-1 bg-surface-container-high rounded-full"></div>
<div className="h-1 flex-1 bg-surface-container-high rounded-full"></div>
<div className="h-1 flex-1 bg-surface-container-high rounded-full"></div>
</div>
{/* Main Core View Area (Scrollable to ensure fit) */}
<div className="flex-grow flex flex-col items-center justify-around px-margin-page pt-6 pb-28 md:pb-32 overflow-y-auto no-scrollbar">
{/* Brand Logo Header */}
<div className="flex flex-col items-center gap-2 mt-2">
<div className="w-20 h-20 bg-primary rounded-2xl flex items-center justify-center shadow-[0_8px_24px_rgba(175,16,26,0.15)] ring-4 ring-white animate-pop-in relative">
<span className="text-white font-display font-extrabold text-2xl tracking-tighter">Pozo</span>
<div className="absolute -top-1.5 -right-1.5 w-5.5 h-5.5 rounded-full bg-amber-500 flex items-center justify-center border-2 border-white">
<span className="text-white text-[9px] font-bold">IN</span>
</div>
</div>
</div>
{/* Headline & Value Proposition */}
<div className="text-center space-y-3 px-2 max-w-sm mt-3">
<h1 className="font-display font-extrabold text-[25px] sm:text-[28px] text-on-surface leading-tight tracking-tight">
Manage your business billing in <span className="text-primary relative inline-block">3 clicks!<span className="absolute bottom-1 left-0 w-full h-[3px] bg-primary-fixed rounded-full -rotate-1"></span></span>
</h1>
<p className="font-sans text-xs sm:text-sm text-on-surface-variant leading-relaxed max-w-[290px] mx-auto">
The fastest, most reliable tool for Indian MSMEs to run their shop.
</p>
<div className="pt-1">
<span className="inline-flex items-center gap-1 text-[11px] sm:text-xs font-semibold text-on-surface-variant bg-surface-container-low px-3 py-1.5 rounded-full border border-surface-container-highest shadow-sm">
<BadgeCheck className="w-3.5 h-3.5 text-tertiary-fixed-dim fill-tertiary" />
Trusted by 10,000+ merchants
</span>
</div>
</div>
{/* Abstract Bento Dashboard Mockup representation */}
<div className="w-full max-w-[280px] aspect-[1/0.95] mt-4 relative flex items-center justify-center">
{/* Simulated dashboard container */}
<div className="absolute inset-0 bg-white rounded-2xl shadow-[0_8px_30px_rgba(0,0,0,0.06)] border border-surface-container-high flex flex-col overflow-hidden transform hover:scale-[1.02] transition-transform duration-300">
{/* Header profile row */}
<div className="h-10 bg-surface-container-low/65 border-b border-surface-container-highest flex items-center px-3 justify-between">
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full bg-brand-red animate-ping"></div>
<div className="w-16 h-2.5 bg-surface-container-high rounded-full"></div>
</div>
<div className="w-4 h-4 rounded-full bg-surface-container-high"></div>
</div>
{/* Action streams inside dashboard model */}
<div className="flex-1 p-3.5 flex flex-col justify-center gap-2.5">
{/* Item row 1: Bill item */}
<div className="w-full h-11 bg-surface-container-low/50 rounded-xl flex items-center px-2.5 gap-2.5 border border-surface-container-high shadow-[inset_0_1px_1px_rgba(255,255,255,0.8)]">
<div className="w-7 h-7 bg-primary/10 rounded-lg flex items-center justify-center text-primary">
<FileText className="w-4 h-4 stroke-[2.2]" />
</div>
<div className="flex-1 space-y-1 text-left">
<div className="w-20 h-2 bg-on-surface-variant rounded-full"></div>
<div className="w-10 h-1.5 bg-outline-variant rounded-full"></div>
</div>
<Percent className="w-3.5 h-3.5 text-outline stroke-[2.5]" />
</div>
{/* Item row 2: Stocks alerts */}
<div className="w-full h-11 bg-surface-container-low/50 rounded-xl flex items-center px-2.5 gap-2.5 border border-surface-container-high">
<div className="w-7 h-7 bg-tertiary/10 rounded-lg flex items-center justify-center text-tertiary">
<TrendingUp className="w-4 h-4 stroke-[2.2]" />
</div>
<div className="flex-1 space-y-1 text-left">
<div className="w-24 h-2 bg-on-surface-variant rounded-full"></div>
<div className="w-14 h-1.5 bg-outline-variant rounded-full"></div>
</div>
<Wallet className="w-3.5 h-3.5 text-outline" />
</div>
</div>
</div>
{/* Floating UI Elements matching Screen 1 overlay cards */}
{/* 1. Green "Saved" checked state card right shadow */}
<div className="absolute -right-3 top-1/4 bg-white py-2 px-3 rounded-xl shadow-[0_8px_20px_rgba(0,0,0,0.12)] border border-surface-container-high flex items-center gap-1.5 transform rotate-3 animate-pulse">
<div className="w-5 h-5 rounded-full bg-tertiary-fixed text-on-tertiary-fixed flex items-center justify-center">
<Check className="w-3 h-3 stroke-[3.5] text-tertiary" />
</div>
<span className="font-sans font-bold text-[11px] text-on-surface">Saved</span>
</div>
{/* 2. Red billing currency tag floating lower left */}
<div className="absolute -left-4 bottom-1/5 bg-primary text-white font-sans font-extrabold text-xs px-3.5 py-1.5 rounded-xl shadow-[0_8px_16px_rgba(175,16,26,0.25)] border border-primary-container transform -rotate-3 hover:rotate-0 transition-transform">
4,500
</div>
</div>
</div>
{/* Pinned Bottom Primary Action Zone */}
<div className="absolute bottom-0 left-0 w-full bg-surface border-t border-surface-container-high shadow-[0_-4px_16px_rgba(0,0,0,0.05)] px-margin-page pt-4 pb-6 z-30">
<div className="max-w-md mx-auto space-y-4">
<button
onClick={onContinue}
className="w-full h-[54px] bg-primary hover:bg-primary-container text-white font-display font-bold text-base rounded-2xl shadow-[0_4px_16px_rgba(175,16,26,0.3)] flex items-center justify-center gap-2 transition-all active:scale-[0.98]"
id="btn-welcome-get-started"
>
<span>Get Started</span>
<ArrowRight className="w-4 h-4 stroke-[2.5]" />
</button>
{/* Dynamic Pagination slider dots */}
<div className="flex justify-center gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-primary transition-all"></div>
<div className="w-2 h-2 rounded-full bg-surface-container-highest"></div>
<div className="w-2 h-2 rounded-full bg-surface-container-highest"></div>
</div>
</div>
</div>
</div>
);
}

107
src/index.css Normal file
View File

@ -0,0 +1,107 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@500;600;700;800&display=swap');
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui;
--font-display: "Plus Jakarta Sans", "Inter", sans-serif;
/* Custom Color Palette from the design tokens */
--color-inverse-on-surface: #f1f1f1;
--color-on-primary-fixed: #410003;
--color-tertiary: #016619;
--color-surface-tint: #ba1a20;
--color-on-secondary-fixed-variant: #004786;
--color-on-primary-container: #fff2f0;
--color-surface-container-lowest: #ffffff;
--color-on-tertiary: #ffffff;
--color-surface-dim: #dadada;
--color-surface-container: #eeeeee;
--color-on-primary: #ffffff;
--color-tertiary-fixed: #9df898;
--color-secondary-fixed-dim: #a5c8ff;
--color-inverse-surface: #2f3131;
--color-inverse-primary: #ffb3ac;
--color-on-background: #1a1c1c;
--color-primary-container: #d32f2f;
--color-on-tertiary-fixed: #002204;
--color-error: #ba1a1a;
--color-surface-bright: #f9f9f9;
--color-on-surface: #1a1c1c;
--color-on-primary-fixed-variant: #930010;
--color-tertiary-fixed-dim: #82db7e;
--color-secondary: #005faf;
--color-on-secondary: #ffffff;
--color-on-secondary-fixed: #001c3a;
--color-primary-fixed-dim: #ffb3ac;
--color-on-tertiary-fixed-variant: #005312;
--color-surface-variant: #e2e2e2;
--color-on-error-container: #93000a;
--color-background: #f9f9f9;
--color-on-secondary-container: #003567;
--color-secondary-container: #54a0fe;
--color-outline-variant: #e4beba;
--color-secondary-fixed: #d4e3ff;
--color-primary: #af101a;
--color-brand-red: #af101a;
--color-brand-red-dark: #86000d;
--color-surface-container-low: #f3f3f3;
--color-on-tertiary-container: #daffd1;
--color-surface: #f9f9f9;
--color-error-container: #ffdad6;
--color-primary-fixed: #ffdad6;
--color-on-surface-variant: #5b403d;
--color-tertiary-container: #298030;
--color-on-error: #ffffff;
--color-surface-container-high: #e8e8e8;
--color-surface-container-highest: #e2e2e2;
--color-outline: #8f6f6c;
}
/* Base custom classes for high fidelity details */
.bg-pattern {
background-image: radial-gradient(#e4beba 0.8px, transparent 0.8px);
background-size: 20px 20px;
}
.bg-pattern-fine {
background-image: radial-gradient(#e2e2e2 1px, transparent 1px);
background-size: 15px 15px;
}
@keyframes popIn {
0% { transform: scale(0); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes fadeInUp {
0% { transform: translateY(15px); opacity: 0; }
100% { transform: translateY(0); opacity: 1; }
}
@keyframes slideLeft {
0% { transform: translateX(20px); opacity: 0; }
100% { transform: translateX(0); opacity: 1; }
}
.animate-pop-in {
animation: popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.animate-fade-in-up {
opacity: 0;
animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-slide-left {
opacity: 0;
animation: slideLeft 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
/* Custom Hide Scrollbar */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}

Some files were not shown because too many files have changed in this diff Show More