Compare commits
2 Commits
770c89ca3f
...
a7e4c5f083
| Author | SHA1 | Date | |
|---|---|---|---|
| a7e4c5f083 | |||
| e87292dda7 |
@@ -6,7 +6,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.yogibook_app"
|
||||
namespace = "it.yogasoul.yogibook"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
@@ -21,7 +21,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// 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.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<application
|
||||
android:label="yogibook_app"
|
||||
android:label="YogiBook"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.example.yogibook_app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,5 @@
|
||||
package it.yogasoul.yogibook
|
||||
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
|
||||
class MainActivity : 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)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -384,7 +384,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -401,7 +401,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -416,7 +416,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -427,7 +427,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
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_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -484,7 +484,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
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_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -547,7 +547,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -569,7 +569,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yogibookApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = it.yogasoul.yogibook;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
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" : "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"
|
||||
}
|
||||
}
|
||||
{"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"}}
|
||||
|
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 |
@@ -28,6 +28,8 @@
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Usiamo Face ID per accedere rapidamente e in sicurezza al tuo account YogiBook.</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
||||
@@ -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? 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) {
|
||||
return School(
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'home_page.dart';
|
||||
import 'meditation_page.dart';
|
||||
import '../widgets/app_drawer.dart';
|
||||
import '../widgets/app_bottom_nav.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../widgets/yogibook_background.dart';
|
||||
import 'reschedule_page.dart';
|
||||
import 'orders_page.dart';
|
||||
@@ -179,40 +180,83 @@ class _LessonsPageState extends State<LessonsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// Bottone "Programma lezioni" (solo se ci sono ticket residui)
|
||||
if (summary != null && summary!.toSchedule > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF10B981),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
label: Text(
|
||||
'Programma lezioni (${summary!.toSchedule} disponibili)',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => OrdersPage(token: widget.token),
|
||||
// Bottoni azione: Programma (condizionale) + Acquista (sempre)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
if (summary != null && summary!.toSchedule > 0) ...[
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF4F46E5),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
);
|
||||
_load();
|
||||
},
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
label: Text(
|
||||
'Programma (${summary!.toSchedule})',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => OrdersPage(token: widget.token),
|
||||
),
|
||||
);
|
||||
_load();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFF59E0B),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.shopping_bag_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: const Text(
|
||||
'Acquista pacchetti',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
final uri = Uri.parse(
|
||||
'https://yogasoul.it/abbonamenti-prezzi-yoga-seregno/',
|
||||
);
|
||||
if (!await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
)) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Impossibile aprire il link'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: loading
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/vanguard_api.dart';
|
||||
import '../services/auth_storage.dart';
|
||||
import '../models/school.dart';
|
||||
import '../config/app_school.dart';
|
||||
import 'home_page.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
@@ -20,15 +22,6 @@ class _LoginPageState extends State<LoginPage> {
|
||||
String error = '';
|
||||
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
|
||||
void dispose() {
|
||||
emailController.dispose();
|
||||
@@ -48,6 +41,9 @@ class _LoginPageState extends State<LoginPage> {
|
||||
password: passwordController.text.trim(),
|
||||
);
|
||||
|
||||
// Se "ricordami" è attivo, salva il nuovo token in cassaforte.
|
||||
await AuthStorage.instance.saveTokenIfRemember(token);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Bypass SelectSchoolPage: scuola unica, si va dritti alla Home.
|
||||
@@ -56,7 +52,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
MaterialPageRoute(
|
||||
builder: (_) => HomePage(
|
||||
token: token,
|
||||
school: _yogaSoulSchool,
|
||||
school: kYogaSoulSchool,
|
||||
userFirstName: null,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +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 'package:flutter/material.dart';
|
||||
|
||||
import '../config/app_school.dart';
|
||||
import '../services/auth_storage.dart';
|
||||
import 'login_page.dart';
|
||||
import 'home_page.dart';
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
const SplashPage({super.key});
|
||||
@@ -15,12 +18,23 @@ class _SplashPageState extends State<SplashPage> {
|
||||
static const Color kRed = Color(0xFFB01E28);
|
||||
|
||||
Timer? _timer;
|
||||
int _seconds = 5; // countdown iniziale
|
||||
|
||||
// Evita doppie navigazioni: countdown, tap e sblocco possono
|
||||
// altrimenti innescare più push.
|
||||
bool _navigated = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// dopo ~2.5s vai al login
|
||||
_timer = Timer(const Duration(milliseconds: 5000), _goToLogin);
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (t) {
|
||||
if (!mounted) return;
|
||||
setState(() => _seconds--);
|
||||
if (_seconds <= 0) {
|
||||
t.cancel();
|
||||
_finishSplash();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -29,8 +43,45 @@ class _SplashPageState extends State<SplashPage> {
|
||||
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() {
|
||||
if (!mounted) return;
|
||||
if (_navigated || !mounted) return;
|
||||
_navigated = true;
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||
@@ -40,9 +91,8 @@ class _SplashPageState extends State<SplashPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// tocca lo schermo per saltare subito al login
|
||||
body: GestureDetector(
|
||||
onTap: _goToLogin,
|
||||
onTap: _onTapSkip,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
@@ -61,7 +111,7 @@ class _SplashPageState extends State<SplashPage> {
|
||||
// Logo YogaSoul
|
||||
Image.asset(
|
||||
'assets/images/yogasoul_logo.png',
|
||||
height: 90,
|
||||
height: 72,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
|
||||
@@ -74,13 +124,11 @@ class _SplashPageState extends State<SplashPage> {
|
||||
vertical: 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
// velo bianco semitrasparente: stacca il testo dal cielo
|
||||
color: Colors.white.withOpacity(0.42),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Titolo riga 1 (scuro)
|
||||
const Text(
|
||||
'Il tuo spazio.',
|
||||
textAlign: TextAlign.center,
|
||||
@@ -91,7 +139,6 @@ class _SplashPageState extends State<SplashPage> {
|
||||
color: Color(0xFF1F1F1F),
|
||||
),
|
||||
),
|
||||
// Titolo riga 2 (rosso bordeaux)
|
||||
const Text(
|
||||
'La tua energia.',
|
||||
textAlign: TextAlign.center,
|
||||
@@ -105,7 +152,6 @@ class _SplashPageState extends State<SplashPage> {
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Sottotitolo
|
||||
const Text(
|
||||
'Ti diamo il benvenuto in YogiBook,\n'
|
||||
'l’app della scuola YogaSoul — Seregno.\n'
|
||||
@@ -126,20 +172,48 @@ class _SplashPageState extends State<SplashPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// ---- Indicatore caricamento in basso
|
||||
const Positioned(
|
||||
// ---- Logo YogiBook + countdown in basso
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 34,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 26,
|
||||
height: 26,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.4,
|
||||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
||||
bottom: 30,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
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),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.25),
|
||||
border: Border.all(color: Colors.white, width: 1.5),
|
||||
),
|
||||
child: Text(
|
||||
'$_seconds',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +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,8 @@ import '../screens/medical_certificates_page.dart';
|
||||
import '../screens/lessons_page.dart';
|
||||
import '../screens/orders_detail_page.dart';
|
||||
import '../screens/account_page.dart';
|
||||
import '../screens/settings_page.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class AppDrawer extends StatelessWidget {
|
||||
final String token;
|
||||
@@ -33,6 +35,16 @@ class AppDrawer extends StatelessWidget {
|
||||
return '${ApiConfig.scheme}://${ApiConfig.host}/public/userarea/$raw';
|
||||
}
|
||||
|
||||
Future<void> _openUrl(BuildContext context, String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
if (!ok && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Impossibile aprire il link.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logout(BuildContext context) async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -155,6 +167,47 @@ 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),
|
||||
|
||||
ListTile(
|
||||
leading: const Icon(Icons.privacy_tip_outlined),
|
||||
title: const Text('Cookie e Privacy Policy'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_openUrl(context, 'https://yogasoul.it/cookie-policy/');
|
||||
},
|
||||
),
|
||||
|
||||
ListTile(
|
||||
leading: const Icon(Icons.description_outlined),
|
||||
title: const Text('Condizioni generali'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_openUrl(
|
||||
context,
|
||||
'https://yogasoul.it/condizioni-generali-di-vendita/',
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(height: 1),
|
||||
|
||||
ListTile(
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_linux
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ import Foundation
|
||||
import audioplayers_darwin
|
||||
import file_picker
|
||||
import file_selector_macos
|
||||
import flutter_secure_storage_macos
|
||||
import google_sign_in_ios
|
||||
import local_auth_darwin
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
@@ -17,7 +19,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -73,6 +89,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -81,6 +105,22 @@ 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:
|
||||
@@ -198,6 +238,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:
|
||||
@@ -219,6 +267,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.33"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.4"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -293,6 +389,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "1976370a4df3091bb0f72409c187ad1f9132a818bc6b95ca59c0bae1c75c688e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.2"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -365,6 +469,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -397,6 +517,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
local_auth:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth
|
||||
sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
local_auth_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.56"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_darwin
|
||||
sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.1"
|
||||
local_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_platform_interface
|
||||
sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
local_auth_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_windows
|
||||
sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.11"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -509,6 +669,14 @@ 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"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -754,6 +922,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.10.4 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
||||
@@ -44,10 +44,14 @@ dependencies:
|
||||
mime: ^2.0.0
|
||||
provider: ^6.1.5+1
|
||||
shared_preferences: ^2.2.3
|
||||
flutter_secure_storage: ^9.2.2
|
||||
local_auth: ^2.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
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
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
@@ -101,3 +105,13 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# 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
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <local_auth_windows/local_auth_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
@@ -15,6 +17,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
LocalAuthPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_windows
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
local_auth_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
||||