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); + }); +}