Initial commit with clean Flutter & React codebase
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
.antigravity/
|
||||
*.zip
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workbench.startupEditor": "readme"
|
||||
}
|
||||
|
|
@ -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`
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 = "../.."
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.example.pozo_app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=false
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
#include "Generated.xcconfig"
|
||||
|
|
@ -0,0 +1 @@
|
|||
#include "Generated.xcconfig"
|
||||
|
|
@ -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 */;
|
||||
}
|
||||
7
flutter_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
BIN
flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
#import "GeneratedPluginRegistrant.h"
|
||||
|
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
After Width: | Height: | Size: 917 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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"]}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||