diff --git a/.gitignore b/.gitignore
index 0858ddd..b8c269a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,6 @@ build/
.idea/
.DS_Store
ios/
-android/
web/
macos/
linux/
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..2d4693b
--- /dev/null
+++ b/.metadata
@@ -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: "84fc5cbb223bc12f83d65b647ff8a56caf779ffd"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
+ base_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
+ - platform: android
+ create_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
+ base_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
+
+ # 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'
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/android/.gitignore
@@ -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
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
new file mode 100644
index 0000000..c81d738
--- /dev/null
+++ b/android/app/build.gradle.kts
@@ -0,0 +1,45 @@
+plugins {
+ id("com.android.application")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.fistinnovations.scalar_attendance"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.fistinnovations.scalar_attendance"
+ // 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")
+ }
+ }
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..6c0716b
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/com/fistinnovations/scalar_attendance/MainActivity.kt b/android/app/src/main/kotlin/com/fistinnovations/scalar_attendance/MainActivity.kt
new file mode 100644
index 0000000..fec336f
--- /dev/null
+++ b/android/app/src/main/kotlin/com/fistinnovations/scalar_attendance/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.fistinnovations.scalar_attendance
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/android/app/src/main/res/drawable-hdpi/android12splash.png b/android/app/src/main/res/drawable-hdpi/android12splash.png
new file mode 100644
index 0000000..064be98
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..442af69
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png
new file mode 100644
index 0000000..a5820ab
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/android12splash.png b/android/app/src/main/res/drawable-mdpi/android12splash.png
new file mode 100644
index 0000000..1acbfd1
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..c61b3b4
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png
new file mode 100644
index 0000000..1469ad1
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-night-hdpi/android12splash.png b/android/app/src/main/res/drawable-night-hdpi/android12splash.png
new file mode 100644
index 0000000..064be98
Binary files /dev/null and b/android/app/src/main/res/drawable-night-hdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/android/app/src/main/res/drawable-night-mdpi/android12splash.png
new file mode 100644
index 0000000..1acbfd1
Binary files /dev/null and b/android/app/src/main/res/drawable-night-mdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-night-xhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png
new file mode 100644
index 0000000..19d0ac1
Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png
new file mode 100644
index 0000000..b59cfa0
Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png
new file mode 100644
index 0000000..290f3f5
Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png
new file mode 100644
index 0000000..8e21404
Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..3cc4948
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,9 @@
+
+
+ -
+
+
+ -
+
+
+
diff --git a/android/app/src/main/res/drawable-xhdpi/android12splash.png b/android/app/src/main/res/drawable-xhdpi/android12splash.png
new file mode 100644
index 0000000..19d0ac1
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..e7b6395
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png
new file mode 100644
index 0000000..56f691b
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxhdpi/android12splash.png
new file mode 100644
index 0000000..b59cfa0
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..823cf61
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png
new file mode 100644
index 0000000..5ee67d6
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png
new file mode 100644
index 0000000..290f3f5
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..019bdac
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png
new file mode 100644
index 0000000..73c3bb4
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png
new file mode 100644
index 0000000..8e21404
Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..3cc4948
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,9 @@
+
+
+ -
+
+
+ -
+
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..c79c58a
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..258e6c5
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..214a9ea
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..5a87aca
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..c710b83
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a5820ab
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml
new file mode 100644
index 0000000..1b8e45f
--- /dev/null
+++ b/android/app/src/main/res/values-night-v31/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..dbc9ea9
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml
new file mode 100644
index 0000000..8012d02
--- /dev/null
+++ b/android/app/src/main/res/values-v31/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c5d5899
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..0d1fa8f
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 0000000..dbee657
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,24 @@
+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("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..e96108c
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,6 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+# This newDsl flag was added by the Flutter template
+android.newDsl=false
+# This builtInKotlin flag was added by the Flutter template
+android.builtInKotlin=false
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..2d428bf
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -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-9.1.0-all.zip
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
new file mode 100644
index 0000000..c21f0c5
--- /dev/null
+++ b/android/settings.gradle.kts
@@ -0,0 +1,26 @@
+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 "9.0.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.3.20" apply false
+}
+
+include(":app")
diff --git a/assets/brand/scalar-knot.png b/assets/brand/scalar-knot.png
new file mode 100644
index 0000000..a09270b
Binary files /dev/null and b/assets/brand/scalar-knot.png differ
diff --git a/assets/brand/scalar-mark-android12.png b/assets/brand/scalar-mark-android12.png
new file mode 100644
index 0000000..484e95f
Binary files /dev/null and b/assets/brand/scalar-mark-android12.png differ
diff --git a/assets/brand/scalar-mark-maskable.png b/assets/brand/scalar-mark-maskable.png
new file mode 100644
index 0000000..8e9230b
Binary files /dev/null and b/assets/brand/scalar-mark-maskable.png differ
diff --git a/assets/brand/scalar-mark.png b/assets/brand/scalar-mark.png
new file mode 100644
index 0000000..8b287d0
Binary files /dev/null and b/assets/brand/scalar-mark.png differ
diff --git a/assets/fonts/poppins/OFL.txt b/assets/fonts/poppins/OFL.txt
new file mode 100644
index 0000000..76df3b5
--- /dev/null
+++ b/assets/fonts/poppins/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/assets/fonts/poppins/Poppins-Bold.ttf b/assets/fonts/poppins/Poppins-Bold.ttf
new file mode 100644
index 0000000..1982f38
Binary files /dev/null and b/assets/fonts/poppins/Poppins-Bold.ttf differ
diff --git a/assets/fonts/poppins/Poppins-Medium.ttf b/assets/fonts/poppins/Poppins-Medium.ttf
new file mode 100644
index 0000000..a590f5c
Binary files /dev/null and b/assets/fonts/poppins/Poppins-Medium.ttf differ
diff --git a/assets/fonts/poppins/Poppins-Regular.ttf b/assets/fonts/poppins/Poppins-Regular.ttf
new file mode 100644
index 0000000..0bda228
Binary files /dev/null and b/assets/fonts/poppins/Poppins-Regular.ttf differ
diff --git a/assets/fonts/poppins/Poppins-SemiBold.ttf b/assets/fonts/poppins/Poppins-SemiBold.ttf
new file mode 100644
index 0000000..c30ad10
Binary files /dev/null and b/assets/fonts/poppins/Poppins-SemiBold.ttf differ
diff --git a/lib/api_service.dart b/lib/api_service.dart
index 8a0ddaf..d9d2962 100644
--- a/lib/api_service.dart
+++ b/lib/api_service.dart
@@ -111,7 +111,7 @@ class ApiService {
ApiService(this.settings);
Uri _uri(String p, [Map? q]) =>
- Uri.parse('${settings.baseUrl}$p').replace(queryParameters: q);
+ Uri.parse('${Settings.baseUrl}$p').replace(queryParameters: q);
Map get _h => {
'Content-Type': 'application/json',
diff --git a/lib/main.dart b/lib/main.dart
index 5553a39..df256d4 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'api_service.dart';
import 'settings.dart';
import 'theme.dart';
+import 'screens/auth_shell.dart';
import 'screens/login_screen.dart';
import 'screens/home_screen.dart';
@@ -9,6 +10,7 @@ Future main() async {
WidgetsFlutterBinding.ensureInitialized();
final settings = Settings();
await settings.load();
+ if (settings.isLoggedIn && !await KeepSignedIn.wanted()) await settings.clearSession();
runApp(ScalarEssApp(settings: settings));
}
@@ -20,7 +22,7 @@ class ScalarEssApp extends StatelessWidget {
Widget build(BuildContext context) {
final api = ApiService(settings);
return MaterialApp(
- title: 'Scalar360 Attendance',
+ title: 'Scalar Presence',
debugShowCheckedModeBanner: false,
theme: buildTheme(),
home: settings.isLoggedIn
diff --git a/lib/screens/auth_shell.dart b/lib/screens/auth_shell.dart
new file mode 100644
index 0000000..d685363
--- /dev/null
+++ b/lib/screens/auth_shell.dart
@@ -0,0 +1,647 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+/// True for test builds pointed at the demo server (shows a "Demo" badge on the sign-in screen).
+const _apiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: '');
+final bool kDemoBuild = _apiBaseUrl.isNotEmpty && !_apiBaseUrl.contains('api.scalar360.com');
+
+/// Light and dark colours for the sign-in screens (Scalar360 brand tokens).
+class AuthPalette {
+ final bool dark;
+ final Color page, surface, text, title, muted, line, border, accent, link, danger, fieldFill;
+ const AuthPalette._({
+ required this.dark,
+ required this.page,
+ required this.surface,
+ required this.text,
+ required this.title,
+ required this.muted,
+ required this.line,
+ required this.border,
+ required this.accent,
+ required this.link,
+ required this.danger,
+ required this.fieldFill,
+ });
+
+ static const light = AuthPalette._(
+ dark: false,
+ page: Color(0xFFFFFFFF),
+ surface: Color(0xFFFFFFFF),
+ text: Color(0xFF1B1B1B),
+ title: Color(0xFF0B2545),
+ muted: Color(0xFF616A75),
+ line: Color(0xFF8A8A8A),
+ border: Color(0xFFD5DCE5),
+ accent: Color(0xFF00965B),
+ link: Color(0xFF00965B),
+ danger: Color(0xFFC62828),
+ fieldFill: Color(0xFFF7F9FB),
+ );
+
+ static const darkPalette = AuthPalette._(
+ dark: true,
+ page: Color(0xFF0E141B),
+ surface: Color(0xFF151D26),
+ text: Color(0xFFE8EDF2),
+ title: Color(0xFFF1F5F9),
+ muted: Color(0xFF9AA6B2),
+ line: Color(0xFF5B6875),
+ border: Color(0xFF2A3542),
+ accent: Color(0xFF00BE73),
+ link: Color(0xFF34D399),
+ danger: Color(0xFFF87171),
+ fieldFill: Color(0xFF1B2530),
+ );
+}
+
+/// Kept for screens that used the old names.
+class AuthColors {
+ static const emerald = Color(0xFF00BE73);
+ static const navy = Color(0xFF0B2545);
+ static const muted = Color(0xFF616A75);
+}
+
+/// The palette for the current sign-in screen.
+class AuthScope extends InheritedWidget {
+ final AuthPalette palette;
+ const AuthScope({super.key, required this.palette, required super.child});
+
+ static AuthScope of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()!;
+
+ @override
+ bool updateShouldNotify(AuthScope old) => old.palette != palette;
+}
+
+/// Light / dark choice for the sign-in screens, remembered on the device ("system" until the user picks).
+class AuthThemeMode {
+ static const _key = 'scalar360.auth.theme';
+ static final notifier = ValueNotifier(ThemeMode.system);
+ static bool _loaded = false;
+
+ static Future load() async {
+ if (_loaded) return;
+ _loaded = true;
+ final v = (await SharedPreferences.getInstance()).getString(_key);
+ notifier.value = v == 'dark' ? ThemeMode.dark : v == 'light' ? ThemeMode.light : ThemeMode.system;
+ }
+
+ static Future set(ThemeMode mode) async {
+ notifier.value = mode;
+ await (await SharedPreferences.getInstance()).setString(_key, mode == ThemeMode.dark ? 'dark' : 'light');
+ }
+}
+
+/// "Keep me signed in": when it was off at sign-in, the saved session is dropped at the next app start.
+class KeepSignedIn {
+ static const _key = 'scalar360.auth.keep_signed_in';
+ static Future save(bool keep) async => (await SharedPreferences.getInstance()).setBool(_key, keep);
+ static Future wanted() async => (await SharedPreferences.getInstance()).getBool(_key) ?? true;
+}
+
+/// Sign-in screen: a brand panel (logo, app name, tagline) above a rounded sheet with the form; light or dark.
+class AuthShell extends StatefulWidget {
+ final String appName;
+ final String title;
+ /// Second line of the brand title under "Scalar360", e.g. "Presence" or "Point of Sale".
+ final String product;
+ /// One line about the app, shown on the brand panel.
+ final String tagline;
+ final List children;
+
+ const AuthShell({
+ super.key,
+ required this.appName,
+ required this.product,
+ this.title = 'Sign in',
+ this.tagline = '',
+ required this.children,
+ });
+
+ @override
+ State createState() => _AuthShellState();
+}
+
+class _AuthShellState extends State {
+ @override
+ void initState() {
+ super.initState();
+ AuthThemeMode.load();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ValueListenableBuilder(
+ valueListenable: AuthThemeMode.notifier,
+ builder: (context, mode, _) {
+ final media = MediaQuery.of(context);
+ final dark = mode == ThemeMode.dark || (mode == ThemeMode.system && media.platformBrightness == Brightness.dark);
+ final palette = dark ? AuthPalette.darkPalette : AuthPalette.light;
+ return AuthScope(
+ palette: palette,
+ child: AnnotatedRegion(
+ // The brand panel is dark in both themes, so the status bar icons are always light.
+ value: SystemUiOverlayStyle.light.copyWith(
+ statusBarColor: Colors.transparent,
+ systemNavigationBarColor: palette.page,
+ systemNavigationBarIconBrightness: dark ? Brightness.light : Brightness.dark,
+ ),
+ child: MediaQuery(
+ data: media.copyWith(textScaler: media.textScaler.clamp(minScaleFactor: 1, maxScaleFactor: 1.3)),
+ child: Theme(
+ data: Theme.of(context).copyWith(
+ brightness: dark ? Brightness.dark : Brightness.light,
+ textSelectionTheme: TextSelectionThemeData(
+ cursorColor: palette.accent,
+ selectionColor: palette.accent.withValues(alpha: 0.3),
+ selectionHandleColor: palette.accent,
+ ),
+ ),
+ child: _PanelLayout(shell: widget, palette: palette),
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
+
+// ------------------------------------------------------------------ brand panel + sheet
+
+class _PanelLayout extends StatelessWidget {
+ final AuthShell shell;
+ final AuthPalette palette;
+ const _PanelLayout({required this.shell, required this.palette});
+
+ static const _gradient = LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [Color(0xFF0B2545), Color(0xFF0B4A3C)],
+ );
+
+ @override
+ Widget build(BuildContext context) {
+ final media = MediaQuery.of(context);
+ final keyboard = media.viewInsets.bottom > 0;
+ // Side by side on tablets and phones held sideways; stacked on phones held upright.
+ final wide = media.size.width >= 720 || (media.size.width > media.size.height && media.size.width >= 560);
+ final side = media.size.width < 360 ? 20.0 : 24.0;
+
+ Widget form({required EdgeInsets padding}) => _FillScroll(
+ child: Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 440),
+ child: Padding(
+ padding: padding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ _Title(shell.title),
+ const SizedBox(height: 20),
+ ...shell.children,
+ const Spacer(),
+ const SizedBox(height: 20),
+ const _Footer(padding: EdgeInsets.zero),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ if (wide) {
+ return Scaffold(
+ backgroundColor: const Color(0xFF0B2545),
+ body: DecoratedBox(
+ decoration: const BoxDecoration(gradient: _gradient),
+ child: SafeArea(
+ child: Row(children: [
+ Expanded(
+ child: _BrandPanel(shell: shell, compact: false, side: 32),
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(0, 16, 16, 16),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(28),
+ child: ColoredBox(
+ color: palette.surface,
+ child: form(padding: const EdgeInsets.fromLTRB(32, 32, 32, 20)),
+ ),
+ ),
+ ),
+ ),
+ ]),
+ ),
+ ),
+ );
+ }
+
+ // Upright phone: the panel (behind the status bar too) over a rounded sheet with the form.
+ final panelBody = keyboard ? 64.0 : (media.size.height * 0.30).clamp(200.0, 270.0);
+ return Scaffold(
+ backgroundColor: palette.surface,
+ body: Column(children: [
+ AnimatedContainer(
+ duration: const Duration(milliseconds: 220),
+ curve: Curves.easeOut,
+ height: media.padding.top + panelBody + 28,
+ decoration: const BoxDecoration(gradient: _gradient),
+ child: Padding(
+ padding: EdgeInsets.only(top: media.padding.top, bottom: 28),
+ child: _BrandPanel(shell: shell, compact: keyboard, side: side),
+ ),
+ ),
+ Expanded(
+ child: Transform.translate(
+ // the sheet's rounded top sits over the panel's lower edge
+ offset: const Offset(0, -28),
+ child: Container(
+ decoration: BoxDecoration(
+ color: palette.surface,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
+ ),
+ child: SafeArea(
+ top: false,
+ child: form(padding: EdgeInsets.fromLTRB(side, 28, side, 12)),
+ ),
+ ),
+ ),
+ ),
+ ]),
+ );
+ }
+}
+
+class _BrandPanel extends StatelessWidget {
+ final AuthShell shell;
+ final bool compact;
+ final double side;
+ const _BrandPanel({required this.shell, required this.compact, required this.side});
+
+ @override
+ Widget build(BuildContext context) {
+ Widget knot(double h) =>
+ Image.asset('assets/brand/scalar-knot.png', height: h, semanticLabel: 'Scalar360', filterQuality: FilterQuality.medium);
+
+ final content = compact
+ ? Row(children: [
+ knot(34),
+ const SizedBox(width: 12),
+ Flexible(
+ child: Text('Scalar360 ${shell.product}',
+ maxLines: 1, overflow: TextOverflow.ellipsis, style: _name(19, FontWeight.w600)),
+ ),
+ ])
+ : Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ knot(60),
+ const SizedBox(height: 14),
+ Text('Scalar360', style: _name(17, FontWeight.w500, opacity: 0.85)),
+ Text(shell.product, style: _name(30, FontWeight.w700, height: 1.15)),
+ if (shell.tagline.isNotEmpty) ...[
+ const SizedBox(height: 8),
+ Text(shell.tagline, style: const TextStyle(fontSize: 14, height: 1.45, color: Color(0xCCFFFFFF))),
+ ],
+ ],
+ );
+
+ return Stack(children: [
+ // soft glow, kept inside the panel
+ Positioned.fill(
+ child: IgnorePointer(
+ child: ClipRect(
+ child: Align(
+ alignment: Alignment.topRight,
+ child: FractionalTranslation(
+ translation: const Offset(0.3, -0.3),
+ child: Container(
+ width: 240,
+ height: 240,
+ decoration: const BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: RadialGradient(colors: [Color(0x5500BE73), Color(0x0000BE73)]),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.fromLTRB(side, 0, side - 12, 0),
+ child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
+ if (!compact)
+ Row(children: [
+ if (kDemoBuild) const _DemoBadge(),
+ const Spacer(),
+ const _ThemeToggle(onDark: true),
+ ]),
+ Expanded(
+ child: compact
+ ? Row(children: [
+ Expanded(child: content),
+ const _ThemeToggle(onDark: true),
+ ])
+ : Padding(
+ padding: const EdgeInsets.only(right: 12),
+ child: Align(
+ alignment: Alignment.centerLeft,
+ // shrinks instead of overflowing on short screens
+ child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: content),
+ ),
+ ),
+ ),
+ ]),
+ ),
+ ]);
+ }
+
+ static TextStyle _name(double size, FontWeight weight, {double opacity = 1, double height = 1.25}) => TextStyle(
+ fontSize: size,
+ fontWeight: weight,
+ height: height,
+ color: Colors.white.withValues(alpha: opacity),
+ letterSpacing: -0.2,
+ );
+}
+
+class _DemoBadge extends StatelessWidget {
+ const _DemoBadge();
+
+ @override
+ Widget build(BuildContext context) => Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
+ decoration: BoxDecoration(color: const Color(0xFFF59E0B), borderRadius: BorderRadius.circular(999)),
+ child: const Text('DEMO', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: Color(0xFF1F2937), letterSpacing: 1)),
+ );
+}
+
+// ------------------------------------------------------------------ shared pieces
+
+/// Scrollable area whose child fills at least the screen height (so Spacers work and nothing overflows).
+class _FillScroll extends StatelessWidget {
+ final Widget child;
+ const _FillScroll({required this.child});
+
+ @override
+ Widget build(BuildContext context) =>
+ CustomScrollView(slivers: [SliverFillRemaining(hasScrollBody: false, child: child)]);
+}
+
+class _Title extends StatelessWidget {
+ final String text;
+ const _Title(this.text);
+
+ @override
+ Widget build(BuildContext context) {
+ final p = AuthScope.of(context).palette;
+ return Text(text,
+ style: TextStyle(fontSize: 26, fontWeight: FontWeight.w600, color: p.title, letterSpacing: -0.3));
+ }
+}
+
+/// Sun / moon button that switches the sign-in screens between light and dark.
+class _ThemeToggle extends StatelessWidget {
+ final bool onDark;
+ const _ThemeToggle({this.onDark = false});
+
+ @override
+ Widget build(BuildContext context) {
+ final p = AuthScope.of(context).palette;
+ final color = onDark ? Colors.white : p.muted;
+ return IconButton(
+ tooltip: p.dark ? 'Light theme' : 'Dark theme',
+ onPressed: () => AuthThemeMode.set(p.dark ? ThemeMode.light : ThemeMode.dark),
+ icon: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 250),
+ transitionBuilder: (child, anim) => RotationTransition(turns: anim, child: FadeTransition(opacity: anim, child: child)),
+ child: Icon(p.dark ? Icons.light_mode_outlined : Icons.dark_mode_outlined,
+ key: ValueKey(p.dark), color: color, size: 22),
+ ),
+ );
+ }
+}
+
+class _Footer extends StatelessWidget {
+ final EdgeInsets padding;
+ const _Footer({this.padding = const EdgeInsets.fromLTRB(24, 8, 24, 16)});
+
+ Future _open(String url) => launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
+
+ @override
+ Widget build(BuildContext context) {
+ final p = AuthScope.of(context).palette;
+ final style = TextStyle(fontSize: 12, color: p.muted);
+ return Padding(
+ padding: padding,
+ child: Wrap(alignment: WrapAlignment.center, spacing: 18, children: [
+ GestureDetector(onTap: () => _open('https://scalar360.com/terms'), child: Text('Terms of use', style: style)),
+ GestureDetector(onTap: () => _open('https://scalar360.com/privacy'), child: Text('Privacy', style: style)),
+ Text('© ${DateTime.now().year} Scalar360', style: style),
+ ]),
+ );
+ }
+}
+
+/// Second step: a back arrow with the identifier entered on the first step.
+class AuthIdentity extends StatelessWidget {
+ final String value;
+ final VoidCallback onBack;
+ const AuthIdentity({super.key, required this.value, required this.onBack});
+
+ @override
+ Widget build(BuildContext context) {
+ final p = AuthScope.of(context).palette;
+ final row = Row(mainAxisSize: MainAxisSize.min, children: [
+ Icon(Icons.arrow_back, size: 18, color: p.text),
+ const SizedBox(width: 8),
+ Flexible(child: Text(value, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 14.5, color: p.text))),
+ ]);
+ return Align(
+ alignment: Alignment.centerLeft,
+ child: InkWell(
+ onTap: onBack,
+ borderRadius: BorderRadius.circular(999),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
+ decoration: BoxDecoration(border: Border.all(color: p.border), borderRadius: BorderRadius.circular(999)),
+ child: row,
+ ),
+ ),
+ );
+ }
+}
+
+/// Rounded, outlined text field with a leading icon.
+class AuthField extends StatefulWidget {
+ final TextEditingController controller;
+ final String hint;
+ final IconData? icon;
+ final bool secret;
+ final bool autofocus;
+ final TextInputType? keyboardType;
+ final TextInputAction? action;
+ final ValueChanged? onSubmitted;
+ final bool error;
+
+ const AuthField({
+ super.key,
+ required this.controller,
+ required this.hint,
+ this.icon,
+ this.secret = false,
+ this.autofocus = false,
+ this.keyboardType,
+ this.action,
+ this.onSubmitted,
+ this.error = false,
+ });
+
+ @override
+ State createState() => _AuthFieldState();
+}
+
+class _AuthFieldState extends State {
+ bool _hidden = true;
+
+ @override
+ Widget build(BuildContext context) {
+ final p = AuthScope.of(context).palette;
+ final base = widget.error ? p.danger : p.border;
+ final focus = widget.error ? p.danger : p.accent;
+ InputBorder border(Color c, double w) =>
+ OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: c, width: w));
+ return TextField(
+ controller: widget.controller,
+ obscureText: widget.secret && _hidden,
+ autofocus: widget.autofocus,
+ autocorrect: false,
+ enableSuggestions: !widget.secret,
+ keyboardType: widget.keyboardType,
+ textInputAction: widget.action,
+ onSubmitted: widget.onSubmitted,
+ style: TextStyle(fontSize: 16, color: p.text),
+ decoration: InputDecoration(
+ hintText: widget.hint,
+ hintStyle: TextStyle(fontSize: 16, color: p.muted),
+ filled: true,
+ fillColor: p.fieldFill,
+ prefixIcon: widget.icon == null ? null : Icon(widget.icon, color: p.muted, size: 20),
+ suffixIcon: widget.secret
+ ? IconButton(
+ tooltip: _hidden ? 'Show' : 'Hide',
+ icon: Icon(_hidden ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: p.muted, size: 20),
+ onPressed: () => setState(() => _hidden = !_hidden),
+ )
+ : null,
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
+ enabledBorder: border(base, 1),
+ focusedBorder: border(focus, 2),
+ border: border(base, 1),
+ ),
+ );
+ }
+}
+
+/// Red message above the field.
+class AuthError extends StatelessWidget {
+ final String message;
+ const AuthError(this.message, {super.key});
+
+ @override
+ Widget build(BuildContext context) => Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: Text(message, style: TextStyle(color: AuthScope.of(context).palette.danger, fontSize: 14)),
+ );
+}
+
+/// Small text link.
+class AuthLink extends StatelessWidget {
+ final String label;
+ final VoidCallback? onPressed;
+ const AuthLink(this.label, {super.key, this.onPressed});
+
+ @override
+ Widget build(BuildContext context) {
+ return Align(
+ alignment: Alignment.centerLeft,
+ child: InkWell(
+ onTap: onPressed,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Text(label, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: AuthScope.of(context).palette.link)),
+ ),
+ ),
+ );
+ }
+}
+
+/// "Keep me signed in" checkbox.
+class AuthKeepSignedIn extends StatelessWidget {
+ final bool value;
+ final ValueChanged onChanged;
+ const AuthKeepSignedIn({super.key, required this.value, required this.onChanged});
+
+ @override
+ Widget build(BuildContext context) {
+ final p = AuthScope.of(context).palette;
+ return InkWell(
+ onTap: () => onChanged(!value),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(children: [
+ SizedBox(
+ width: 20,
+ height: 20,
+ child: Checkbox(
+ value: value,
+ onChanged: (v) => onChanged(v ?? true),
+ activeColor: p.accent,
+ side: BorderSide(color: p.line, width: 1.5),
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ visualDensity: VisualDensity.compact,
+ ),
+ ),
+ const SizedBox(width: 10),
+ Text('Keep me signed in', style: TextStyle(fontSize: 14, color: p.text)),
+ ]),
+ ),
+ );
+ }
+}
+
+/// Full-width button for the step, with a spinner while busy.
+class AuthButton extends StatelessWidget {
+ final String label;
+ final bool busy;
+ final VoidCallback onPressed;
+ const AuthButton({super.key, required this.label, required this.busy, required this.onPressed});
+
+ @override
+ Widget build(BuildContext context) {
+ final p = AuthScope.of(context).palette;
+ return FilledButton(
+ onPressed: busy ? null : onPressed,
+ style: FilledButton.styleFrom(
+ backgroundColor: p.accent,
+ disabledBackgroundColor: p.accent.withValues(alpha: 0.6),
+ foregroundColor: Colors.white,
+ minimumSize: const Size.fromHeight(52),
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
+ // same family as the rest of the screen (Poppins), which a button style does not inherit by itself
+ textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, fontFamily: DefaultTextStyle.of(context).style.fontFamily),
+ elevation: 0,
+ ),
+ child: busy
+ ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2.2, color: Colors.white))
+ : Text(label),
+ );
+ }
+}
diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart
index a10df36..bb0b6f5 100644
--- a/lib/screens/login_screen.dart
+++ b/lib/screens/login_screen.dart
@@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import '../api_service.dart';
import '../settings.dart';
-import '../theme.dart';
+import 'auth_shell.dart';
import 'home_screen.dart';
-import 'settings_screen.dart';
class LoginScreen extends StatefulWidget {
final ApiService api;
@@ -19,15 +18,32 @@ class _LoginScreenState extends State {
final _pin = TextEditingController();
bool _busy = false;
String? _error;
+ /// Zoho-style: the email first, then the PIN on its own step.
+ bool _askPin = false;
+ bool _keep = true;
+
+ static final _emailPattern = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
+
+ void _next() {
+ final email = _email.text.trim();
+ if (!_emailPattern.hasMatch(email)) {
+ setState(() => _error = 'Enter a valid work email address');
+ return;
+ }
+ setState(() { _askPin = true; _error = null; });
+ }
+
+ void _change() => setState(() { _askPin = false; _pin.clear(); _error = null; });
Future _login() async {
- if (_email.text.trim().isEmpty || _pin.text.isEmpty) {
- setState(() => _error = 'Enter your work email and PIN');
+ if (_pin.text.isEmpty) {
+ setState(() => _error = 'Enter your PIN');
return;
}
setState(() { _busy = true; _error = null; });
try {
- await widget.api.login(_email.text, _pin.text);
+ await widget.api.login(_email.text.trim(), _pin.text);
+ await KeepSignedIn.save(_keep);
if (!mounted) return;
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (_) => HomeScreen(api: widget.api, settings: widget.settings)));
@@ -38,82 +54,73 @@ class _LoginScreenState extends State {
}
}
+ void _forgotEmail() {
+ ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+ content: Text('Use the work email HR added you with. Not sure which one? Ask your HR or manager.'),
+ behavior: SnackBarBehavior.floating,
+ ));
+ }
+
+ void _forgotPin() {
+ ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+ content: Text('Your PIN is set by HR. Ask your HR or manager to reset it.'),
+ behavior: SnackBarBehavior.floating,
+ ));
+ }
+
@override
Widget build(BuildContext context) {
- return Scaffold(
- body: SafeArea(
- child: Center(
- child: SingleChildScrollView(
- padding: const EdgeInsets.all(24),
- child: ConstrainedBox(
- constraints: const BoxConstraints(maxWidth: 420),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
- Row(children: [
- Container(
- width: 40, height: 40,
- decoration: BoxDecoration(color: AppColors.emerald, borderRadius: BorderRadius.circular(12)),
- child: const Icon(Icons.badge_outlined, color: Colors.white),
- ),
- const SizedBox(width: 10),
- const Text('Scalar360', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.navy)),
- ]),
- IconButton(
- icon: const Icon(Icons.settings_outlined, color: AppColors.muted),
- onPressed: () => Navigator.push(context,
- MaterialPageRoute(builder: (_) => SettingsScreen(settings: widget.settings))),
- ),
- ]),
- const SizedBox(height: 28),
- const Text('Employee sign in',
- style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold, color: AppColors.navy)),
- const SizedBox(height: 4),
- const Text('Use your work email and the PIN from HR', style: TextStyle(color: AppColors.muted)),
- const SizedBox(height: 24),
- TextField(
- controller: _email,
- keyboardType: TextInputType.emailAddress,
- autocorrect: false,
- decoration: const InputDecoration(labelText: 'Work email', prefixIcon: Icon(Icons.mail_outline)),
- ),
- const SizedBox(height: 14),
- TextField(
- controller: _pin,
- obscureText: true,
- keyboardType: TextInputType.number,
- onSubmitted: (_) => _login(),
- decoration: const InputDecoration(labelText: 'PIN', prefixIcon: Icon(Icons.pin_outlined)),
- ),
- if (_error != null) ...[
- const SizedBox(height: 14),
- Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(color: AppColors.danger.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(12)),
- child: Row(children: [
- const Icon(Icons.error_outline, color: AppColors.danger, size: 18),
- const SizedBox(width: 8),
- Expanded(child: Text(_error!, style: const TextStyle(color: AppColors.danger))),
- ]),
- ),
- ],
- const SizedBox(height: 22),
- FilledButton(
- onPressed: _busy ? null : _login,
- child: _busy
- ? const SizedBox(height: 22, width: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
- : const Text('Sign in'),
- ),
- const SizedBox(height: 16),
- Text('Server: ${widget.settings.baseUrl}',
- textAlign: TextAlign.center, style: const TextStyle(color: AppColors.muted, fontSize: 12)),
- ],
- ),
- ),
+ if (_askPin) {
+ return AuthShell(
+ appName: 'Scalar Presence',
+ product: 'Presence',
+ tagline: 'Check in, apply for leave and see your payslips.',
+ title: 'Enter PIN',
+ children: [
+ AuthIdentity(value: _email.text.trim(), onBack: _busy ? () {} : _change),
+ const SizedBox(height: 16),
+ if (_error != null) AuthError(_error!),
+ AuthField(
+ key: const ValueKey('pin'),
+ controller: _pin,
+ hint: 'PIN',
+ icon: Icons.lock_outline,
+ secret: true,
+ autofocus: true,
+ error: _error != null,
+ keyboardType: TextInputType.number,
+ action: TextInputAction.done,
+ onSubmitted: (_) => _login(),
),
+ const SizedBox(height: 12),
+ AuthLink('Forgot PIN?', onPressed: _forgotPin),
+ AuthKeepSignedIn(value: _keep, onChanged: (v) => setState(() => _keep = v)),
+ const SizedBox(height: 20),
+ AuthButton(label: 'Sign in', busy: _busy, onPressed: _login),
+ ],
+ );
+ }
+ return AuthShell(
+ appName: 'Scalar Presence',
+ product: 'Presence',
+ tagline: 'Check in, apply for leave and see your payslips.',
+ children: [
+ if (_error != null) AuthError(_error!),
+ AuthField(
+ key: const ValueKey('email'),
+ controller: _email,
+ hint: 'Work email',
+ icon: Icons.mail_outline,
+ error: _error != null,
+ keyboardType: TextInputType.emailAddress,
+ action: TextInputAction.next,
+ onSubmitted: (_) => _next(),
),
- ),
+ const SizedBox(height: 12),
+ AuthLink('Forgot email?', onPressed: _forgotEmail),
+ const SizedBox(height: 28),
+ AuthButton(label: 'Next', busy: false, onPressed: _next),
+ ],
);
}
}
diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart
index 8c7d997..8a1d119 100644
--- a/lib/screens/settings_screen.dart
+++ b/lib/screens/settings_screen.dart
@@ -12,14 +12,6 @@ class SettingsScreen extends StatefulWidget {
}
class _SettingsScreenState extends State {
- late final TextEditingController _url = TextEditingController(text: widget.settings.baseUrl);
-
- void _save() {
- widget.settings.baseUrl = _url.text.trim();
- ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Saved')));
- setState(() {});
- }
-
@override
Widget build(BuildContext context) {
final name = widget.settings.name;
@@ -36,22 +28,8 @@ class _SettingsScreenState extends State {
subtitle: Text(widget.settings.isManager ? 'Manager' : 'Employee'),
),
),
- const SizedBox(height: 20),
- const Text('Server URL', style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.navy)),
- const SizedBox(height: 8),
- TextField(
- controller: _url,
- keyboardType: TextInputType.url,
- autocorrect: false,
- decoration: const InputDecoration(prefixIcon: Icon(Icons.link), hintText: Settings.defaultBaseUrl),
- ),
- const SizedBox(height: 6),
- const Text('The Scalar360 API base URL. Default is api.scalar360.in.',
- style: TextStyle(fontSize: 12, color: AppColors.muted)),
- const SizedBox(height: 16),
- FilledButton(onPressed: _save, child: const Text('Save')),
if (widget.onLogout != null) ...[
- const SizedBox(height: 32),
+ const SizedBox(height: 20),
OutlinedButton.icon(
onPressed: widget.onLogout,
icon: const Icon(Icons.logout, color: AppColors.danger),
diff --git a/lib/settings.dart b/lib/settings.dart
index 534a123..d0e2a03 100644
--- a/lib/settings.dart
+++ b/lib/settings.dart
@@ -2,19 +2,17 @@ import 'package:shared_preferences/shared_preferences.dart';
/// Persisted app settings + employee session.
class Settings {
- static const _kBaseUrl = 'base_url';
static const _kToken = 'emp_token';
static const _kName = 'emp_name';
static const _kRole = 'emp_role';
- static const defaultBaseUrl = 'https://api.scalar360.in';
+ /// Scalar360 API. Fixed per build (not shown to or changed by the user); a test build points at demo with
+ /// `--dart-define=API_BASE_URL=https://demo.scalar360.in`.
+ static const baseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: 'https://api.scalar360.com');
late SharedPreferences _prefs;
Future load() async => _prefs = await SharedPreferences.getInstance();
- String get baseUrl => (_prefs.getString(_kBaseUrl) ?? defaultBaseUrl).trim();
- set baseUrl(String v) => _prefs.setString(_kBaseUrl, v.trim());
-
String? get token => _prefs.getString(_kToken);
String? get name => _prefs.getString(_kName);
String get role => _prefs.getString(_kRole) ?? 'EMPLOYEE';
diff --git a/lib/theme.dart b/lib/theme.dart
index 48a0298..1f2cf09 100644
--- a/lib/theme.dart
+++ b/lib/theme.dart
@@ -38,7 +38,7 @@ ThemeData buildTheme() {
brightness: Brightness.light,
),
scaffoldBackgroundColor: AppColors.surface,
- fontFamily: 'Roboto',
+ fontFamily: 'Poppins',
);
return base.copyWith(
appBarTheme: const AppBarTheme(
diff --git a/pubspec.lock b/pubspec.lock
index d5970db..86abcc9 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1,6 +1,30 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ ansicolor:
+ dependency: transitive
+ description:
+ name: ansicolor
+ sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.3"
+ archive:
+ dependency: transitive
+ description:
+ name: archive
+ sha256: "0d58d1fa1fddf6588251ccf2819f3fb3b6d0452c1b2813ac7413a05592bfbefe"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.5.2"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.7.0"
async:
dependency: transitive
description:
@@ -17,14 +41,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.4"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.2"
clock:
dependency: transitive
description:
name: clock
- sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
+ sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e
url: "https://pub.dev"
source: hosted
- version: "1.1.2"
+ version: "1.1.3"
collection:
dependency: transitive
description:
@@ -37,10 +77,10 @@ packages:
dependency: transitive
description:
name: cross_file
- sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
+ sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6
url: "https://pub.dev"
source: hosted
- version: "0.3.5+4"
+ version: "0.3.5+5"
crypto:
dependency: transitive
description:
@@ -49,6 +89,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.7"
+ csslib:
+ dependency: transitive
+ description:
+ name: csslib
+ sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
@@ -77,18 +125,18 @@ packages:
dependency: transitive
description:
name: file_selector_linux
- sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
+ sha256: da76400e7872ce7637ffdce12749ec24169c25f6195c28372208e65a24bcd2ab
url: "https://pub.dev"
source: hosted
- version: "0.9.4"
+ version: "0.9.4+1"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
- sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
+ sha256: d57c62362766b5e7ae739448650b66c6aab7a68ba7ecc65e04018652645ae0f4
url: "https://pub.dev"
source: hosted
- version: "0.9.5"
+ version: "0.9.5+1"
file_selector_platform_interface:
dependency: transitive
description:
@@ -101,10 +149,10 @@ packages:
dependency: transitive
description:
name: file_selector_windows
- sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
+ sha256: fbefc5fb92c6d3cbe8d284a2cd971b593bb07d2cd6da8557b81a862250b4acec
url: "https://pub.dev"
source: hosted
- version: "0.9.3+5"
+ version: "0.9.3+6"
fixnum:
dependency: transitive
description:
@@ -118,6 +166,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_launcher_icons:
+ dependency: "direct dev"
+ description:
+ name: flutter_launcher_icons
+ sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -126,6 +182,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
+ flutter_native_splash:
+ dependency: "direct dev"
+ description:
+ name: flutter_native_splash
+ sha256: "9db4b80b044e9af17cc4b1272137fc7ace0054d879ef8210a76adc34aaf4cdff"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.8"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -159,18 +223,18 @@ packages:
dependency: transitive
description:
name: geolocator_apple
- sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
+ sha256: "3b3189beba8b59b2a0eee970b466e991e4ac1564a9034f82a825a3c45c4be1fa"
url: "https://pub.dev"
source: hosted
- version: "2.3.14"
+ version: "2.3.15"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
- sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
+ sha256: b640b6b687b14c66f51fe1130b1c2e8babbd408e0158052b92006b6675ca77fc
url: "https://pub.dev"
source: hosted
- version: "4.2.8"
+ version: "4.4.0"
geolocator_web:
dependency: transitive
description:
@@ -187,6 +251,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.5"
+ html:
+ dependency: transitive
+ description:
+ name: html
+ sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.15.7"
http:
dependency: "direct main"
description:
@@ -203,22 +275,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
+ image:
+ dependency: transitive
+ description:
+ name: image
+ sha256: "89ed75b2f8231fe676808c5564f9590d728024ded7ac782a7ed8474f1c95b72a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.12.0"
image_picker:
dependency: "direct main"
description:
name: image_picker
- sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667
+ sha256: e71b1bd9e40124041128d60fc3034c7b1598e1bba4e570bba0c70301c817013a
url: "https://pub.dev"
source: hosted
- version: "1.2.3"
+ version: "1.2.4"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
- sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
+ sha256: "8a7823d5d3e884f7780829d85d92110d072bdcd9ed48e2be158d2abe91a912d4"
url: "https://pub.dev"
source: hosted
- version: "0.8.13+19"
+ version: "0.8.13+25"
image_picker_for_web:
dependency: transitive
description:
@@ -231,10 +311,10 @@ packages:
dependency: transitive
description:
name: image_picker_ios
- sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
+ sha256: "4fb88d4350ffc208c2a09fc321db1f80282688716b8cfe10c8c80acc5ae8934a"
url: "https://pub.dev"
source: hosted
- version: "0.8.13+6"
+ version: "0.8.13+10"
image_picker_linux:
dependency: transitive
description:
@@ -255,10 +335,10 @@ packages:
dependency: transitive
description:
name: image_picker_platform_interface
- sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
+ sha256: "2dee4b4e3be429db71472c9c559c981498070a61af1ba82e0e4986fc5dadc042"
url: "https://pub.dev"
source: hosted
- version: "2.11.1"
+ version: "2.11.2"
image_picker_windows:
dependency: transitive
description:
@@ -275,6 +355,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.19.0"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.12.0"
lints:
dependency: transitive
description:
@@ -303,10 +391,10 @@ packages:
dependency: transitive
description:
name: mime
- sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
+ sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6
url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "2.1.0"
path:
dependency: transitive
description:
@@ -339,14 +427,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.2"
platform:
dependency: transitive
description:
name: platform
- sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
+ sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec
url: "https://pub.dev"
source: hosted
- version: "3.1.6"
+ version: "3.2.0"
plugin_platform_interface:
dependency: transitive
description:
@@ -355,16 +451,24 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
+ posix:
+ dependency: transitive
+ description:
+ name: posix
+ sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.5.2"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
- sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
+ sha256: "1e85e0dccf5b4a016077fea7dfbb4a7cb39c759ac1596fcf60c06deadb7da91f"
url: "https://pub.dev"
source: hosted
- version: "2.5.5"
+ version: "2.5.6"
shared_preferences_android:
- dependency: transitive
+ dependency: "direct main"
description:
name: shared_preferences_android
sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7"
@@ -375,10 +479,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_foundation
- sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
+ sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea"
url: "https://pub.dev"
source: hosted
- version: "2.5.6"
+ version: "2.5.7"
shared_preferences_linux:
dependency: transitive
description:
@@ -448,6 +552,78 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
+ universal_io:
+ dependency: transitive
+ description:
+ name: universal_io
+ sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.1"
+ url_launcher:
+ dependency: "direct main"
+ description:
+ name: url_launcher
+ sha256: e8cc955c97e06df9879140a1fe301f3d493f01b7c62dec7f46834be31d7e9fda
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.3"
+ url_launcher_android:
+ dependency: transitive
+ description:
+ name: url_launcher_android
+ sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.33"
+ url_launcher_ios:
+ dependency: transitive
+ description:
+ name: url_launcher_ios
+ sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.4.2"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.3"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.6"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.3"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.6"
uuid:
dependency: transitive
description:
@@ -480,6 +656,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.1"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.4"
sdks:
dart: ">=3.12.0 <4.0.0"
flutter: ">=3.44.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 12ef940..0cd83dd 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -10,14 +10,49 @@ dependencies:
flutter:
sdk: flutter
http: ^1.2.0
+ url_launcher: ^6.3.0
shared_preferences: ^2.2.0
+ # 2.5.x crashes at start on release builds with Flutter 3.44 (NoSuchMethodError via JNI); POS uses 2.4.27 fine
+ shared_preferences_android: 2.4.27
intl: ^0.19.0
geolocator: ^13.0.1
image_picker: ^1.1.2
cupertino_icons: ^1.0.6
dev_dependencies:
+ flutter_launcher_icons: ^0.14.0
+ flutter_native_splash: ^2.4.0
flutter_lints: ^4.0.0
+flutter_launcher_icons:
+ android: true
+ ios: false
+ image_path: "assets/brand/scalar-mark.png"
+ adaptive_icon_background: "#FFFFFF"
+ adaptive_icon_foreground: "assets/brand/scalar-mark-maskable.png"
+ min_sdk_android: 21
+
+flutter_native_splash:
+ color: "#FFFFFF"
+ image: assets/brand/scalar-mark.png
+ android_12:
+ color: "#FFFFFF"
+ image: assets/brand/scalar-mark-android12.png
+ icon_background_color: "#FFFFFF"
+ ios: false
+ web: false
+
flutter:
uses-material-design: true
+ assets:
+ - assets/brand/
+ fonts:
+ - family: Poppins
+ fonts:
+ - asset: assets/fonts/poppins/Poppins-Regular.ttf
+ - asset: assets/fonts/poppins/Poppins-Medium.ttf
+ weight: 500
+ - asset: assets/fonts/poppins/Poppins-SemiBold.ttf
+ weight: 600
+ - asset: assets/fonts/poppins/Poppins-Bold.ttf
+ weight: 700
diff --git a/test/widget_test.dart b/test/widget_test.dart
new file mode 100644
index 0000000..c2cff6a
--- /dev/null
+++ b/test/widget_test.dart
@@ -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:scalar_attendance/main.dart';
+
+void main() {
+ testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(const MyApp());
+
+ // 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);
+ });
+}