fixed things and icon
@@ -6,7 +6,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.yogibook_app"
|
namespace = "it.yogasoul.yogibook"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId = "com.example.yogibook_app"
|
applicationId = "it.yogasoul.yogibook"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<application
|
<application
|
||||||
android:label="yogibook_app"
|
android:label="YogiBook"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.yogibook_app
|
package it.yogasoul.yogibook
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 184 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground>
|
||||||
|
<inset
|
||||||
|
android:drawable="@drawable/ic_launcher_foreground"
|
||||||
|
android:inset="16%" />
|
||||||
|
</foreground>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 371 KiB |
@@ -368,7 +368,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp;
|
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -384,7 +384,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -401,7 +401,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@@ -416,7 +416,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@@ -427,7 +427,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
@@ -484,7 +484,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
@@ -547,7 +547,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp;
|
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -569,7 +569,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp;
|
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|||||||
@@ -1,122 +1 @@
|
|||||||
{
|
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-20x20@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-60x60@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-60x60@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "83.5x83.5",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "1024x1024",
|
|
||||||
"idiom" : "ios-marketing",
|
|
||||||
"filename" : "Icon-App-1024x1024@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 856 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
import '../models/school.dart';
|
||||||
|
|
||||||
|
/// Scuola unica dell'app (YogaSoul).
|
||||||
|
/// Il DB non ha il concetto di "scuola": questo oggetto serve solo alle
|
||||||
|
/// pagine che mostrano nome/indirizzo. Definito qui una volta sola così
|
||||||
|
/// login e splash (e chiunque altro) usano lo stesso dato.
|
||||||
|
const School kYogaSoulSchool = School(
|
||||||
|
id: 1,
|
||||||
|
name: 'YogaSoul',
|
||||||
|
logo: null,
|
||||||
|
addressFull: 'via Valassina 62/B Seregno - Sala Contesto Yoga',
|
||||||
|
);
|
||||||
@@ -4,7 +4,12 @@ class School {
|
|||||||
final String? logo;
|
final String? logo;
|
||||||
final String? addressFull;
|
final String? addressFull;
|
||||||
|
|
||||||
School({required this.id, required this.name, this.logo, this.addressFull});
|
const School({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.logo,
|
||||||
|
this.addressFull,
|
||||||
|
});
|
||||||
|
|
||||||
factory School.fromJson(Map<String, dynamic> j) {
|
factory School.fromJson(Map<String, dynamic> j) {
|
||||||
return School(
|
return School(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../services/vanguard_api.dart';
|
import '../services/vanguard_api.dart';
|
||||||
|
import '../services/auth_storage.dart';
|
||||||
import '../models/school.dart';
|
import '../models/school.dart';
|
||||||
|
import '../config/app_school.dart';
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
@@ -20,15 +22,6 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
String error = '';
|
String error = '';
|
||||||
bool _obscurePassword = true;
|
bool _obscurePassword = true;
|
||||||
|
|
||||||
// Scuola unica (YogaSoul). Il DB non ha il concetto di scuola:
|
|
||||||
// questo oggetto serve solo a soddisfare le pagine che mostrano nome/indirizzo.
|
|
||||||
static final School _yogaSoulSchool = School(
|
|
||||||
id: 1,
|
|
||||||
name: 'YogaSoul',
|
|
||||||
logo: null,
|
|
||||||
addressFull: 'via Valassina 62/B Seregno - Sala Contesto Yoga',
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
emailController.dispose();
|
emailController.dispose();
|
||||||
@@ -48,6 +41,9 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
password: passwordController.text.trim(),
|
password: passwordController.text.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Se "ricordami" è attivo, salva il nuovo token in cassaforte.
|
||||||
|
await AuthStorage.instance.saveTokenIfRemember(token);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
// Bypass SelectSchoolPage: scuola unica, si va dritti alla Home.
|
// Bypass SelectSchoolPage: scuola unica, si va dritti alla Home.
|
||||||
@@ -56,7 +52,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => HomePage(
|
builder: (_) => HomePage(
|
||||||
token: token,
|
token: token,
|
||||||
school: _yogaSoulSchool,
|
school: kYogaSoulSchool,
|
||||||
userFirstName: null,
|
userFirstName: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1 +1,238 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../models/school.dart';
|
||||||
|
import '../services/auth_storage.dart';
|
||||||
|
import '../widgets/yogibook_background.dart';
|
||||||
|
|
||||||
|
class SettingsPage extends StatefulWidget {
|
||||||
|
final String token;
|
||||||
|
final School school;
|
||||||
|
final String? userFirstName;
|
||||||
|
|
||||||
|
const SettingsPage({
|
||||||
|
super.key,
|
||||||
|
required this.token,
|
||||||
|
required this.school,
|
||||||
|
this.userFirstName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsPage> createState() => _SettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsPageState extends State<SettingsPage> {
|
||||||
|
static const Color kGreen = Color(0xFF10B981);
|
||||||
|
|
||||||
|
final _auth = AuthStorage.instance;
|
||||||
|
|
||||||
|
bool _loading = true;
|
||||||
|
bool _rememberMe = false;
|
||||||
|
bool _biometric = false;
|
||||||
|
bool _biometricAvailableOnDevice = false;
|
||||||
|
|
||||||
|
// evita doppi tap durante il salvataggio
|
||||||
|
bool _busy = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final remember = await _auth.isRememberMeEnabled();
|
||||||
|
final bio = await _auth.isBiometricEnabled();
|
||||||
|
final canBio = await _auth.canUseBiometrics();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_rememberMe = remember;
|
||||||
|
_biometric = bio;
|
||||||
|
_biometricAvailableOnDevice = canBio;
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _snack(String msg) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Ricordami": se acceso salva SUBITO il token corrente in cassaforte,
|
||||||
|
// così funziona già da questa sessione (non solo dal prossimo login).
|
||||||
|
Future<void> _onRememberChanged(bool value) async {
|
||||||
|
if (_busy) return;
|
||||||
|
setState(() => _busy = true);
|
||||||
|
try {
|
||||||
|
await _auth.setRememberMe(value);
|
||||||
|
if (value) {
|
||||||
|
// salva il token che questa pagina ha in mano
|
||||||
|
await _auth.saveTokenIfRemember(widget.token);
|
||||||
|
}
|
||||||
|
// spegnere ricordami spegne anche la biometria (dipendenza)
|
||||||
|
final bioNow = await _auth.isBiometricEnabled();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_rememberMe = value;
|
||||||
|
_biometric = bioNow;
|
||||||
|
});
|
||||||
|
_snack(
|
||||||
|
value ? 'Accesso rapido attivato.' : 'Accesso rapido disattivato.',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _busy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onBiometricChanged(bool value) async {
|
||||||
|
if (_busy) return;
|
||||||
|
|
||||||
|
// non attivabile senza impronte registrate / hardware
|
||||||
|
if (value && !_biometricAvailableOnDevice) {
|
||||||
|
_snack(
|
||||||
|
'Nessuna impronta o volto registrati sul dispositivo. '
|
||||||
|
'Configurali nelle impostazioni del telefono, poi riprova.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _busy = true);
|
||||||
|
try {
|
||||||
|
await _auth.setBiometric(value);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _biometric = value);
|
||||||
|
_snack(
|
||||||
|
value
|
||||||
|
? 'Sblocco biometrico attivato.'
|
||||||
|
: 'Sblocco biometrico disattivato.',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _busy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
|
title: const Text(
|
||||||
|
'Impostazioni',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: YogibookBackground(
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: _loading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: ListView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||||
|
children: [
|
||||||
|
_sectionTitle('Accesso'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_accessCard(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_infoNote(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _sectionTitle(String t) => Text(
|
||||||
|
t,
|
||||||
|
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w900),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _accessCard() {
|
||||||
|
// la biometria è attivabile solo se "ricordami" è ON
|
||||||
|
final biometricEnabled = _rememberMe;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 18,
|
||||||
|
color: Color(0x12000000),
|
||||||
|
offset: Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SwitchListTile(
|
||||||
|
activeColor: kGreen,
|
||||||
|
value: _rememberMe,
|
||||||
|
onChanged: _busy ? null : _onRememberChanged,
|
||||||
|
secondary: const Icon(Icons.vpn_key_rounded),
|
||||||
|
title: const Text(
|
||||||
|
'Ricordami',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w800),
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Resta connesso su questo dispositivo senza reinserire la password.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
SwitchListTile(
|
||||||
|
activeColor: kGreen,
|
||||||
|
value: _biometric && biometricEnabled,
|
||||||
|
onChanged: (_busy || !biometricEnabled)
|
||||||
|
? null
|
||||||
|
: _onBiometricChanged,
|
||||||
|
secondary: Icon(
|
||||||
|
Icons.fingerprint_rounded,
|
||||||
|
color: biometricEnabled ? null : Colors.black26,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'Sblocco con impronta / volto',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: biometricEnabled ? null : Colors.black38,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
biometricEnabled
|
||||||
|
? 'Chiedi la biometria all\'avvio per accedere.'
|
||||||
|
: 'Attiva prima "Ricordami" per usare lo sblocco biometrico.',
|
||||||
|
style: TextStyle(color: biometricEnabled ? null : Colors.black38),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _infoNote() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF6F6FB),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.info_outline, size: 20, color: Colors.black45),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_biometricAvailableOnDevice
|
||||||
|
? 'Il token di accesso è salvato in forma cifrata sul dispositivo. '
|
||||||
|
'Con lo sblocco biometrico attivo, viene usato solo dopo il riconoscimento.'
|
||||||
|
: 'Questo dispositivo non ha impronta o volto configurati: '
|
||||||
|
'lo sblocco biometrico non è disponibile, ma "Ricordami" funziona lo stesso.',
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.black54),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../config/app_school.dart';
|
||||||
|
import '../services/auth_storage.dart';
|
||||||
import 'login_page.dart';
|
import 'login_page.dart';
|
||||||
|
import 'home_page.dart';
|
||||||
|
|
||||||
class SplashPage extends StatefulWidget {
|
class SplashPage extends StatefulWidget {
|
||||||
const SplashPage({super.key});
|
const SplashPage({super.key});
|
||||||
@@ -17,16 +20,19 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
int _seconds = 5; // countdown iniziale
|
int _seconds = 5; // countdown iniziale
|
||||||
|
|
||||||
|
// Evita doppie navigazioni: countdown, tap e sblocco possono
|
||||||
|
// altrimenti innescare più push.
|
||||||
|
bool _navigated = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// countdown 5 → 0, poi vai al login
|
|
||||||
_timer = Timer.periodic(const Duration(seconds: 1), (t) {
|
_timer = Timer.periodic(const Duration(seconds: 1), (t) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _seconds--);
|
setState(() => _seconds--);
|
||||||
if (_seconds <= 0) {
|
if (_seconds <= 0) {
|
||||||
t.cancel();
|
t.cancel();
|
||||||
_goToLogin();
|
_finishSplash();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -37,8 +43,45 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fine dello splash: prova lo sblocco, poi instrada.
|
||||||
|
Future<void> _finishSplash() async {
|
||||||
|
if (_navigated) return;
|
||||||
|
|
||||||
|
final outcome = await AuthStorage.instance.tryUnlockAtStartup();
|
||||||
|
if (!mounted || _navigated) return;
|
||||||
|
|
||||||
|
switch (outcome.result) {
|
||||||
|
case UnlockResult.success:
|
||||||
|
_navigated = true;
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => HomePage(
|
||||||
|
token: outcome.token!,
|
||||||
|
school: kYogaSoulSchool,
|
||||||
|
userFirstName: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case UnlockResult.noSavedLogin:
|
||||||
|
case UnlockResult.biometricFailed:
|
||||||
|
case UnlockResult.biometricUnavailable:
|
||||||
|
_goToLogin();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tap sullo schermo: salta il countdown ma passa comunque dallo sblocco.
|
||||||
|
void _onTapSkip() {
|
||||||
|
if (_navigated) return;
|
||||||
|
_timer?.cancel();
|
||||||
|
_finishSplash();
|
||||||
|
}
|
||||||
|
|
||||||
void _goToLogin() {
|
void _goToLogin() {
|
||||||
if (!mounted) return;
|
if (_navigated || !mounted) return;
|
||||||
|
_navigated = true;
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||||
@@ -48,9 +91,8 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// tocca lo schermo per saltare subito al login
|
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
onTap: _goToLogin,
|
onTap: _onTapSkip,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
@@ -69,7 +111,7 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
// Logo YogaSoul
|
// Logo YogaSoul
|
||||||
Image.asset(
|
Image.asset(
|
||||||
'assets/images/yogasoul_logo.png',
|
'assets/images/yogasoul_logo.png',
|
||||||
height: 90,
|
height: 72,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -82,13 +124,11 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
vertical: 16,
|
vertical: 16,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
// velo bianco semitrasparente: stacca il testo dal cielo
|
|
||||||
color: Colors.white.withOpacity(0.42),
|
color: Colors.white.withOpacity(0.42),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Titolo riga 1 (scuro)
|
|
||||||
const Text(
|
const Text(
|
||||||
'Il tuo spazio.',
|
'Il tuo spazio.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -99,7 +139,6 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
color: Color(0xFF1F1F1F),
|
color: Color(0xFF1F1F1F),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Titolo riga 2 (rosso bordeaux)
|
|
||||||
const Text(
|
const Text(
|
||||||
'La tua energia.',
|
'La tua energia.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -113,7 +152,6 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
// Sottotitolo
|
|
||||||
const Text(
|
const Text(
|
||||||
'Ti diamo il benvenuto in YogiBook,\n'
|
'Ti diamo il benvenuto in YogiBook,\n'
|
||||||
'l’app della scuola YogaSoul — Seregno.\n'
|
'l’app della scuola YogaSoul — Seregno.\n'
|
||||||
@@ -141,13 +179,22 @@ class _SplashPageState extends State<SplashPage> {
|
|||||||
bottom: 30,
|
bottom: 30,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Image.asset(
|
Container(
|
||||||
'assets/images/yogibook_logo.png',
|
padding: const EdgeInsets.symmetric(
|
||||||
height: 40,
|
horizontal: 16,
|
||||||
fit: BoxFit.contain,
|
vertical: 10,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.75),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/yogibook_logo.png',
|
||||||
|
height: 40,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
// Countdown numerico dentro un cerchio
|
|
||||||
Container(
|
Container(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
|
|||||||
@@ -1 +1,167 @@
|
|||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:local_auth/local_auth.dart';
|
||||||
|
import 'package:local_auth/error_codes.dart' as auth_error;
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// Esito dello sblocco all'avvio.
|
||||||
|
enum UnlockResult {
|
||||||
|
/// Nessun login salvato: mostra la schermata di login normale.
|
||||||
|
noSavedLogin,
|
||||||
|
|
||||||
|
/// Token disponibile e pronto all'uso: entra diretto.
|
||||||
|
success,
|
||||||
|
|
||||||
|
/// C'è un login salvato ma la biometria è fallita/annullata:
|
||||||
|
/// resta sul login, l'utente può accedere con password.
|
||||||
|
biometricFailed,
|
||||||
|
|
||||||
|
/// Biometria richiesta ma non disponibile (niente impronte / hardware):
|
||||||
|
/// fallback a login con password (scelta B).
|
||||||
|
biometricUnavailable,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custode unico di token + preferenze di accesso (ricordami + biometria).
|
||||||
|
///
|
||||||
|
/// Non è un provider: lo si interroga in momenti precisi
|
||||||
|
/// (avvio nello splash, login, logout). Vedi nota architetturale nel piano.
|
||||||
|
class AuthStorage {
|
||||||
|
AuthStorage._();
|
||||||
|
static final AuthStorage instance = AuthStorage._();
|
||||||
|
|
||||||
|
// Su Android forza l'uso di EncryptedSharedPreferences (più robusto).
|
||||||
|
static const _secure = FlutterSecureStorage(
|
||||||
|
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
final LocalAuthentication _localAuth = LocalAuthentication();
|
||||||
|
|
||||||
|
// Chiavi interne della cassaforte.
|
||||||
|
static const _kToken = 'auth_token';
|
||||||
|
static const _kRemember = 'pref_remember_me';
|
||||||
|
static const _kBiometric = 'pref_biometric';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PREFERENZE
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// "Ricordami" attivo? (default: false)
|
||||||
|
Future<bool> isRememberMeEnabled() async {
|
||||||
|
return (await _secure.read(key: _kRemember)) == 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Biometria attiva? (default: false)
|
||||||
|
Future<bool> isBiometricEnabled() async {
|
||||||
|
return (await _secure.read(key: _kBiometric)) == 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Imposta "ricordami". Se lo spegni, cancella anche il token salvato:
|
||||||
|
/// senza ricordami non ha senso tenere un token in cassaforte.
|
||||||
|
Future<void> setRememberMe(bool value) async {
|
||||||
|
await _secure.write(key: _kRemember, value: value ? 'true' : 'false');
|
||||||
|
if (!value) {
|
||||||
|
await _secure.delete(key: _kToken);
|
||||||
|
// Biometria senza token salvato non ha nulla da proteggere: spegnila.
|
||||||
|
await _secure.write(key: _kBiometric, value: 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Imposta la biometria. Ha senso solo se "ricordami" è attivo.
|
||||||
|
Future<void> setBiometric(bool value) async {
|
||||||
|
await _secure.write(key: _kBiometric, value: value ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// TOKEN
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Salva il token SOLO se "ricordami" è attivo. Chiamato dopo il login.
|
||||||
|
Future<void> saveTokenIfRemember(String token) async {
|
||||||
|
if (await isRememberMeEnabled()) {
|
||||||
|
await _secure.write(key: _kToken, value: token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancella tutto: token + preferenze. Chiamato al logout.
|
||||||
|
Future<void> clear() async {
|
||||||
|
await _secure.delete(key: _kToken);
|
||||||
|
await _secure.delete(key: _kRemember);
|
||||||
|
await _secure.delete(key: _kBiometric);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lettura grezza del token (senza biometria). Uso interno.
|
||||||
|
Future<String?> _readToken() => _secure.read(key: _kToken);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// BIOMETRIA
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Il dispositivo può fare biometria E ha almeno un metodo registrato?
|
||||||
|
Future<bool> canUseBiometrics() async {
|
||||||
|
try {
|
||||||
|
final supported = await _localAuth.isDeviceSupported();
|
||||||
|
if (!supported) return false;
|
||||||
|
final canCheck = await _localAuth.canCheckBiometrics;
|
||||||
|
if (!canCheck) return false;
|
||||||
|
final enrolled = await _localAuth.getAvailableBiometrics();
|
||||||
|
return enrolled.isNotEmpty;
|
||||||
|
} on PlatformException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chiede l'impronta/volto. true = passata, false = fallita/annullata.
|
||||||
|
Future<bool> _promptBiometric() async {
|
||||||
|
try {
|
||||||
|
return await _localAuth.authenticate(
|
||||||
|
localizedReason: 'Sblocca YogiBook per accedere al tuo account',
|
||||||
|
options: const AuthenticationOptions(
|
||||||
|
biometricOnly: true, // niente fallback al PIN del telefono (scelta B)
|
||||||
|
stickyAuth: true, // regge se l'app va in background durante il prompt
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
// Casi tipici: nessuna impronta registrata, hardware assente, troppi tentativi.
|
||||||
|
if (e.code == auth_error.notAvailable ||
|
||||||
|
e.code == auth_error.notEnrolled ||
|
||||||
|
e.code == auth_error.lockedOut ||
|
||||||
|
e.code == auth_error.permanentlyLockedOut) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SBLOCCO ALL'AVVIO (chiamato dallo splash)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Decide cosa fare all'avvio dell'app.
|
||||||
|
/// Restituisce l'esito; il token (se disponibile) è in [tokenOut].
|
||||||
|
Future<({UnlockResult result, String? token})> tryUnlockAtStartup() async {
|
||||||
|
// 1. Ricordami spento o nessun token → login normale.
|
||||||
|
if (!await isRememberMeEnabled()) {
|
||||||
|
return (result: UnlockResult.noSavedLogin, token: null);
|
||||||
|
}
|
||||||
|
final token = await _readToken();
|
||||||
|
if (token == null || token.isEmpty) {
|
||||||
|
return (result: UnlockResult.noSavedLogin, token: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Biometria spenta → entra diretto col token salvato.
|
||||||
|
if (!await isBiometricEnabled()) {
|
||||||
|
return (result: UnlockResult.success, token: token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Biometria accesa ma non disponibile → fallback a password (scelta B).
|
||||||
|
if (!await canUseBiometrics()) {
|
||||||
|
return (result: UnlockResult.biometricUnavailable, token: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Biometria accesa e disponibile → chiedi l'impronta.
|
||||||
|
final ok = await _promptBiometric();
|
||||||
|
if (ok) {
|
||||||
|
return (result: UnlockResult.success, token: token);
|
||||||
|
}
|
||||||
|
return (result: UnlockResult.biometricFailed, token: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import '../screens/medical_certificates_page.dart';
|
|||||||
import '../screens/lessons_page.dart';
|
import '../screens/lessons_page.dart';
|
||||||
import '../screens/orders_detail_page.dart';
|
import '../screens/orders_detail_page.dart';
|
||||||
import '../screens/account_page.dart';
|
import '../screens/account_page.dart';
|
||||||
|
import '../screens/settings_page.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class AppDrawer extends StatelessWidget {
|
class AppDrawer extends StatelessWidget {
|
||||||
@@ -166,6 +167,24 @@ class AppDrawer extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.settings_rounded),
|
||||||
|
title: const Text('Impostazioni'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => SettingsPage(
|
||||||
|
token: token,
|
||||||
|
school: school,
|
||||||
|
userFirstName: userFirstName,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: be169cf6ac481e052c4538715d88841d567150dfe1df38aaec76461a4e7b39f2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -73,6 +89,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
change_app_package_name:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: change_app_package_name
|
||||||
|
sha256: "8e43b754fe960426904d77ed4c62fa8c9834deaf6e293ae40963fa447482c4c5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -81,6 +105,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
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:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -198,6 +238,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -341,6 +389,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: "1976370a4df3091bb0f72409c187ad1f9132a818bc6b95ca59c0bae1c75c688e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.9.2"
|
||||||
image_picker:
|
image_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -421,6 +477,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.7"
|
version: "0.6.7"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.12.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -605,6 +669,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.2"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -850,6 +922,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.4 <4.0.0"
|
dart: ">=3.10.4 <4.0.0"
|
||||||
flutter: ">=3.35.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
change_app_package_name: ^1.5.0
|
||||||
|
flutter_launcher_icons: ^0.14.4
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
@@ -103,3 +105,13 @@ flutter:
|
|||||||
#
|
#
|
||||||
# For details regarding fonts from package dependencies,
|
# For details regarding fonts from package dependencies,
|
||||||
# see https://flutter.dev/to/font-from-package
|
# see https://flutter.dev/to/font-from-package
|
||||||
|
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: true
|
||||||
|
ios: true
|
||||||
|
image_path: "assets/icon/app_icon.png"
|
||||||
|
# Adaptive icon Android: sfondo bianco + soggetto in primo piano.
|
||||||
|
adaptive_icon_background: "#FFFFFF"
|
||||||
|
adaptive_icon_foreground: "assets/icon/app_icon.png"
|
||||||
|
# iOS non ammette trasparenza: rimuove il canale alpha automaticamente.
|
||||||
|
remove_alpha_ios: true
|
||||||
|
|||||||