CHECKPOINT: Working signed release build with Google authentication
This commit is contained in:
parent
78d3d45570
commit
81064da7f4
|
|
@ -54,3 +54,9 @@ node_modules/
|
|||
scratch/
|
||||
flutter-doctor.txt
|
||||
gemini-mcp-config-search.txt
|
||||
|
||||
# Android signing credentials & local properties
|
||||
*.jks
|
||||
*.keystore
|
||||
android/key.properties
|
||||
android/local.properties
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
val hasReleaseKeystore = if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.motorsignal.motorcycle"
|
||||
compileSdk = 37
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.motorsignal.motorcycle"
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
if (hasReleaseKeystore) {
|
||||
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||
val storeFilePath = keystoreProperties.getProperty("storeFile")
|
||||
storeFile = rootProject.file(storeFilePath)
|
||||
storePassword = keystoreProperties.getProperty("storePassword")
|
||||
storeType = "PKCS12"
|
||||
}
|
||||
// If hasReleaseKeystore is false, properties are intentionally left null.
|
||||
// The release buildType below will throw before the signing step is reached.
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (!hasReleaseKeystore) {
|
||||
throw GradleException(
|
||||
"Release build requires android/key.properties with production keystore credentials. " +
|
||||
"Copy android/key.properties.template to android/key.properties and fill in your keystore details."
|
||||
)
|
||||
}
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "809431485362",
|
||||
"project_id": "motoapp-d22e0",
|
||||
"storage_bucket": "motoapp-d22e0.firebasestorage.app"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:809431485362:android:06fdcd9ca12ff9342472ab",
|
||||
"android_client_info": {
|
||||
"package_name": "app.motorsignal.motorcycle"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "809431485362-c2qfdas53jtoft3udu6jokciv9pruehq.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "app.motorsignal.motorcycle",
|
||||
"certificate_hash": "251eff25a99d0687e10e3f021f5d3c6e21d9bb7d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "809431485362-g7tpuiersfc47e8qfbk1pe8bihu01pbk.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "app.motorsignal.motorcycle",
|
||||
"certificate_hash": "07105037193d1bed3f53924d0f50b37cab435f0e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "809431485362-ar11dsgjrq7327d122b55352gs6q6sml.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyD-ie8zTaAWvFUlCG5bHqRhfHKOcu4jPtg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "809431485362-ar11dsgjrq7327d122b55352gs6q6sml.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:809431485362:android:27df3f15fc35c1152472ab",
|
||||
"android_client_info": {
|
||||
"package_name": "com.example.motorcycle_dash"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "809431485362-s1k8u0brkm6mqf9a4fgfj345rdscifd9.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.example.motorcycle_dash",
|
||||
"certificate_hash": "251eff25a99d0687e10e3f021f5d3c6e21d9bb7d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "809431485362-ar11dsgjrq7327d122b55352gs6q6sml.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyD-ie8zTaAWvFUlCG5bHqRhfHKOcu4jPtg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "809431485362-ar11dsgjrq7327d122b55352gs6q6sml.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:label="motorcycle_dash"
|
||||
android:label="MotorSignal"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<activity
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.example.motorcycle_dash
|
||||
package app.motorsignal.motorcycle
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,8 @@
|
|||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
android.useAndroidX=true
|
||||
# This newDsl flag was added by the Flutter template
|
||||
android.newDsl=false
|
||||
# This builtInKotlin flag was added by the Flutter template
|
||||
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ plugins {
|
|||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "9.0.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class DashboardScreen extends ConsumerWidget {
|
|||
child: Column(
|
||||
children: [
|
||||
if (!hasLocPermission) _buildPermissionBanner(context, ref),
|
||||
if (closestAlert != null) _buildAlertBanner(closestAlert),
|
||||
if (closestAlert != null) _buildAlertBanner(context, closestAlert, ref),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
|
|
@ -102,10 +102,10 @@ class DashboardScreen extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildAlertBanner(RoadAlert alert) {
|
||||
Widget _buildAlertBanner(BuildContext context, RoadAlert alert, WidgetRef ref) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: alert.category.color.withValues(alpha: 0.2),
|
||||
border: Border(bottom: BorderSide(color: alert.category.color, width: 2)),
|
||||
|
|
@ -120,15 +120,36 @@ class DashboardScreen extends ConsumerWidget {
|
|||
children: [
|
||||
Text(
|
||||
alert.category.displayName.toUpperCase(),
|
||||
style: TextStyle(color: alert.category.color, fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.2),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: alert.category.color,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${alert.distanceFromUser?.toStringAsFixed(0) ?? '?'}m Ahead',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
|
||||
'${alert.distanceFromUser?.toStringAsFixed(0) ?? '?'}m Ahead • ${alert.roadName ?? 'Reported'}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white70, size: 20),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
tooltip: 'Dismiss alert',
|
||||
onPressed: () {
|
||||
ref.read(roadAlertServiceProvider).dismissAlert(alert.id);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,12 +15,16 @@ class OAuthCallbackScreen extends ConsumerStatefulWidget {
|
|||
class _OAuthCallbackScreenState extends ConsumerState<OAuthCallbackScreen> {
|
||||
String _status = 'Connecting to OpenStreetMap...';
|
||||
bool _isSuccess = false;
|
||||
bool _hasProcessed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_processAuth();
|
||||
if (!_hasProcessed) {
|
||||
_hasProcessed = true;
|
||||
_processAuth();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -289,10 +289,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'OpenStreetMap Account',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'OpenStreetMap Account',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Chip(
|
||||
label: Text(
|
||||
isAuthed ? 'Connected' : 'Disconnected',
|
||||
|
|
|
|||
|
|
@ -106,24 +106,32 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.palette_outlined, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'ACTIVE THEME:',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white70,
|
||||
const Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.palette_outlined, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'ACTIVE THEME:',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SegmentedButton<DashboardThemeMode>(
|
||||
showSelectedIcon: false,
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity.compact,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
|
|
@ -268,22 +276,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.account_circle, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'MOTORCLOUD ACCOUNT',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFF00E5FF),
|
||||
letterSpacing: 0.1,
|
||||
const Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.account_circle, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'MOTORCLOUD ACCOUNT',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFF00E5FF),
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -398,22 +412,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.public, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'OPENSTREETMAP ACCOUNT',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFF00E5FF),
|
||||
letterSpacing: 0.1,
|
||||
const Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.public, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'OPENSTREETMAP ACCOUNT',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFF00E5FF),
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -775,14 +795,17 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
|||
children: [
|
||||
Icon(Icons.screen_rotation, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'SENSORS & MOUNT CALIBRATION',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
Expanded(
|
||||
child: Text(
|
||||
'SENSORS & MOUNT CALIBRATION',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -983,22 +1006,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.biotech, color: isEnabled ? Colors.greenAccent : const Color(0xFF00E5FF), size: 18),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'ENGINEERING & DIAGNOSTICS',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.biotech, color: isEnabled ? Colors.greenAccent : const Color(0xFF00E5FF), size: 18),
|
||||
const SizedBox(width: 8),
|
||||
const Flexible(
|
||||
child: Text(
|
||||
'ENGINEERING & DIAGNOSTICS',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Switch(
|
||||
value: isEnabled,
|
||||
activeThumbColor: Colors.greenAccent,
|
||||
|
|
@ -1118,22 +1147,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.cloud_upload_outlined, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'GITHUB RIDE LOG SYNC',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
const Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.cloud_upload_outlined, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'GITHUB RIDE LOG SYNC',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -1255,22 +1290,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.traffic, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'TOMTOM TRAFFIC PROVIDER',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
const Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.traffic, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'TOMTOM TRAFFIC PROVIDER',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatusBadge(tomtomStatus),
|
||||
],
|
||||
),
|
||||
|
|
@ -1352,22 +1393,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.local_police, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'COMMUNITY TRAFFIC PROVIDER',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
const Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.local_police, color: Color(0xFF00E5FF), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'COMMUNITY TRAFFIC PROVIDER',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatusBadge(communityStatus),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
// File generated for MotorSignal Firebase Configuration.
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||
import 'package:flutter/foundation.dart'
|
||||
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||
|
||||
/// Default [FirebaseOptions] for use with your Firebase apps.
|
||||
class DefaultFirebaseOptions {
|
||||
static FirebaseOptions get currentPlatform {
|
||||
if (kIsWeb) {
|
||||
return web;
|
||||
}
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
return android;
|
||||
case TargetPlatform.iOS:
|
||||
return ios;
|
||||
default:
|
||||
return android;
|
||||
}
|
||||
}
|
||||
|
||||
static const FirebaseOptions android = FirebaseOptions(
|
||||
apiKey: 'AIzaSyD-ie8zTaAWvFUlCG5bHqRhfHKOcu4jPtg',
|
||||
appId: '1:809431485362:android:06fdcd9ca12ff9342472ab',
|
||||
messagingSenderId: '809431485362',
|
||||
projectId: 'motoapp-d22e0',
|
||||
storageBucket: 'motoapp-d22e0.firebasestorage.app',
|
||||
);
|
||||
|
||||
static const FirebaseOptions web = FirebaseOptions(
|
||||
apiKey: 'AIzaSyD-ie8zTaAWvFUlCG5bHqRhfHKOcu4jPtg',
|
||||
appId: '1:809431485362:web:06fdcd9ca12ff9342472ab',
|
||||
messagingSenderId: '809431485362',
|
||||
projectId: 'motoapp-d22e0',
|
||||
storageBucket: 'motoapp-d22e0.firebasestorage.app',
|
||||
);
|
||||
|
||||
static const FirebaseOptions ios = FirebaseOptions(
|
||||
apiKey: 'AIzaSyD-ie8zTaAWvFUlCG5bHqRhfHKOcu4jPtg',
|
||||
appId: '1:809431485362:ios:06fdcd9ca12ff9342472ab',
|
||||
messagingSenderId: '809431485362',
|
||||
projectId: 'motoapp-d22e0',
|
||||
storageBucket: 'motoapp-d22e0.firebasestorage.app',
|
||||
);
|
||||
}
|
||||
|
|
@ -4,17 +4,19 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'firebase_options.dart';
|
||||
import 'core/theme/theme.dart';
|
||||
import 'core/router.dart';
|
||||
import 'features/records/records_service.dart';
|
||||
import 'services/osm_oauth_service.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Optional Firebase initialization (gracefully defaults to standalone offline mode if unconfigured)
|
||||
// Authoritative Firebase initialization
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[Firebase] Standalone offline mode active: $e');
|
||||
}
|
||||
|
|
@ -67,7 +69,6 @@ class _MotorcycleDashAppState extends ConsumerState<MotorcycleDashApp> {
|
|||
void _handleDeepLink(Uri uri) {
|
||||
final uriStr = uri.toString();
|
||||
if (uriStr.contains('callback') || uri.host == 'oauth' || uri.scheme == 'motorsignal') {
|
||||
ref.read(osmOAuthServiceProvider.notifier).handleAuthCallback(uri);
|
||||
try {
|
||||
appRouter.go('/oauth/callback?${uri.query}');
|
||||
} catch (_) {}
|
||||
|
|
|
|||
|
|
@ -34,12 +34,17 @@ class RoadAlertService {
|
|||
|
||||
List<RoadAlert> _cachedAlerts = [];
|
||||
final Set<String> _announcedAlertIds = {};
|
||||
final Set<String> _dismissedAlertIds = {};
|
||||
Timer? _cleanupTimer;
|
||||
|
||||
Stream<List<RoadAlert>> get alertsStream => _alertsController.stream;
|
||||
|
||||
RoadAlertService(this._ref) {
|
||||
_initTts();
|
||||
_startListening();
|
||||
_cleanupTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||||
_pruneExpiredAlerts();
|
||||
});
|
||||
}
|
||||
|
||||
void _initTts() {
|
||||
|
|
@ -308,7 +313,7 @@ class RoadAlertService {
|
|||
description: 'TEST ALERT: ${category.displayName} reported ahead (Diagnostic Verification)',
|
||||
roadName: 'Test Way / Simulation',
|
||||
reportedAt: now,
|
||||
expiresAt: now.add(const Duration(minutes: 5)),
|
||||
expiresAt: now.add(const Duration(seconds: 12)),
|
||||
lastUpdated: now,
|
||||
distanceFromUser: 150.0,
|
||||
source: 'Internal Pipeline Test',
|
||||
|
|
@ -337,6 +342,50 @@ class RoadAlertService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Dismisses a specific alert by ID and prevents it from re-appearing
|
||||
void dismissAlert(String alertId) {
|
||||
_dismissedAlertIds.add(alertId);
|
||||
_cachedAlerts = _cachedAlerts.where((a) => a.id != alertId && '${a.providerId}_${a.id}' != alertId).toList();
|
||||
_alertsController.add(_cachedAlerts);
|
||||
if (kDebugMode) {
|
||||
debugPrint('[RoadAlerts] Manually dismissed alert $alertId');
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears all cached alerts immediately
|
||||
void clearAllAlerts() {
|
||||
_cachedAlerts = [];
|
||||
_alertsController.add([]);
|
||||
if (kDebugMode) {
|
||||
debugPrint('[RoadAlerts] Cleared all cached alerts');
|
||||
}
|
||||
}
|
||||
|
||||
/// Periodically prunes expired or dismissed alerts even when stationary
|
||||
void _pruneExpiredAlerts() {
|
||||
if (_cachedAlerts.isEmpty) return;
|
||||
final now = DateTime.now();
|
||||
final initialCount = _cachedAlerts.length;
|
||||
|
||||
_cachedAlerts = _cachedAlerts.where((alert) {
|
||||
final key = '${alert.providerId}_${alert.id}';
|
||||
if (_dismissedAlertIds.contains(alert.id) || _dismissedAlertIds.contains(key)) {
|
||||
return false;
|
||||
}
|
||||
if (alert.expiresAt != null && alert.expiresAt!.isBefore(now)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
if (_cachedAlerts.length != initialCount) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[RoadAlerts] Pruned expired/dismissed alerts (remaining: ${_cachedAlerts.length})');
|
||||
}
|
||||
_alertsController.add(_cachedAlerts);
|
||||
}
|
||||
}
|
||||
|
||||
void simulateAlert(RoadAlert alert) {
|
||||
if (!_ref.read(roadAlertsEnabledProvider)) return;
|
||||
_cachedAlerts = [alert, ..._cachedAlerts];
|
||||
|
|
@ -350,6 +399,7 @@ class RoadAlertService {
|
|||
if (kDebugMode) {
|
||||
debugPrint('[RoadAlerts] Disposing service resources');
|
||||
}
|
||||
_cleanupTimer?.cancel();
|
||||
_locationSub?.cancel();
|
||||
_alertsController.close();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ abstract class AuthService {
|
|||
|
||||
/// Authoritative Firebase Authentication Service
|
||||
class FirebaseAuthService implements AuthService {
|
||||
final fb.FirebaseAuth? _firebaseAuth;
|
||||
fb.FirebaseAuth? _firebaseAuth;
|
||||
final GoogleSignIn? _googleSignIn;
|
||||
|
||||
final _stateController = StreamController<AuthState>.broadcast();
|
||||
|
|
@ -60,10 +60,18 @@ class FirebaseAuthService implements AuthService {
|
|||
AuthUser? get currentUser => _currentState.user;
|
||||
|
||||
@override
|
||||
bool get isConfigured => _firebaseAuth != null;
|
||||
bool get isConfigured {
|
||||
if (_firebaseAuth != null) return true;
|
||||
_firebaseAuth = _resolveFirebaseAuth();
|
||||
if (_firebaseAuth != null && _fbAuthSubscription == null) {
|
||||
_init();
|
||||
}
|
||||
return _firebaseAuth != null;
|
||||
}
|
||||
|
||||
void _init() {
|
||||
if (_firebaseAuth == null) {
|
||||
final auth = _firebaseAuth;
|
||||
if (auth == null) {
|
||||
// Firebase is not configured or offline during startup
|
||||
_updateState(const AuthState.unauthenticated(isFirebaseConfigured: false));
|
||||
return;
|
||||
|
|
@ -71,7 +79,7 @@ class FirebaseAuthService implements AuthService {
|
|||
|
||||
try {
|
||||
// Listen to native Firebase auth state stream
|
||||
_fbAuthSubscription = _firebaseAuth.authStateChanges().listen(
|
||||
_fbAuthSubscription = auth.authStateChanges().listen(
|
||||
(fb.User? user) {
|
||||
if (user == null) {
|
||||
_updateState(const AuthState.unauthenticated(isFirebaseConfigured: true));
|
||||
|
|
@ -125,7 +133,8 @@ class FirebaseAuthService implements AuthService {
|
|||
|
||||
@override
|
||||
Future<void> signInWithEmailAndPassword(String email, String password) async {
|
||||
if (!isConfigured || _firebaseAuth == null) {
|
||||
final auth = _firebaseAuth;
|
||||
if (!isConfigured || auth == null) {
|
||||
_updateState(AuthState.error(
|
||||
'Cloud authentication is unavailable in offline/unconfigured mode. Local ride recording is active.',
|
||||
user: _currentState.user,
|
||||
|
|
@ -140,7 +149,7 @@ class FirebaseAuthService implements AuthService {
|
|||
));
|
||||
|
||||
try {
|
||||
final credential = await _firebaseAuth.signInWithEmailAndPassword(
|
||||
final credential = await auth.signInWithEmailAndPassword(
|
||||
email: email.trim(),
|
||||
password: password,
|
||||
);
|
||||
|
|
@ -163,7 +172,8 @@ class FirebaseAuthService implements AuthService {
|
|||
String password, {
|
||||
String? displayName,
|
||||
}) async {
|
||||
if (!isConfigured || _firebaseAuth == null) {
|
||||
final auth = _firebaseAuth;
|
||||
if (!isConfigured || auth == null) {
|
||||
_updateState(AuthState.error(
|
||||
'Account registration requires cloud configuration. Local riding remains fully functional.',
|
||||
user: _currentState.user,
|
||||
|
|
@ -178,7 +188,7 @@ class FirebaseAuthService implements AuthService {
|
|||
));
|
||||
|
||||
try {
|
||||
final credential = await _firebaseAuth.createUserWithEmailAndPassword(
|
||||
final credential = await auth.createUserWithEmailAndPassword(
|
||||
email: email.trim(),
|
||||
password: password,
|
||||
);
|
||||
|
|
@ -200,7 +210,8 @@ class FirebaseAuthService implements AuthService {
|
|||
|
||||
@override
|
||||
Future<void> signInWithGoogle() async {
|
||||
if (!isConfigured || _firebaseAuth == null) {
|
||||
final auth = _firebaseAuth;
|
||||
if (!isConfigured || auth == null) {
|
||||
_updateState(AuthState.error(
|
||||
'Google Sign-In is unavailable without Firebase credentials. Local ride features remain available.',
|
||||
user: _currentState.user,
|
||||
|
|
@ -230,7 +241,7 @@ class FirebaseAuthService implements AuthService {
|
|||
idToken: googleAuth.idToken,
|
||||
);
|
||||
|
||||
final result = await _firebaseAuth.signInWithCredential(credential);
|
||||
final result = await auth.signInWithCredential(credential);
|
||||
if (result.user != null) {
|
||||
final user = _mapFirebaseUser(result.user!);
|
||||
_updateState(AuthState.authenticated(user, isFirebaseConfigured: true));
|
||||
|
|
@ -246,7 +257,8 @@ class FirebaseAuthService implements AuthService {
|
|||
|
||||
@override
|
||||
Future<void> signInAnonymously() async {
|
||||
if (!isConfigured || _firebaseAuth == null) {
|
||||
final auth = _firebaseAuth;
|
||||
if (!isConfigured || auth == null) {
|
||||
// Local fallback anonymous user representation
|
||||
final localAnon = AuthUser(
|
||||
uid: 'local_${DateTime.now().millisecondsSinceEpoch}',
|
||||
|
|
@ -264,7 +276,7 @@ class FirebaseAuthService implements AuthService {
|
|||
));
|
||||
|
||||
try {
|
||||
final credential = await _firebaseAuth.signInAnonymously();
|
||||
final credential = await auth.signInAnonymously();
|
||||
if (credential.user != null) {
|
||||
final user = _mapFirebaseUser(credential.user!);
|
||||
_updateState(AuthState.anonymous(user, isFirebaseConfigured: true));
|
||||
|
|
@ -280,7 +292,8 @@ class FirebaseAuthService implements AuthService {
|
|||
|
||||
@override
|
||||
Future<void> sendPasswordResetEmail(String email) async {
|
||||
if (!isConfigured || _firebaseAuth == null) {
|
||||
final auth = _firebaseAuth;
|
||||
if (!isConfigured || auth == null) {
|
||||
_updateState(AuthState.error(
|
||||
'Password reset is unavailable in offline mode.',
|
||||
user: _currentState.user,
|
||||
|
|
@ -290,7 +303,7 @@ class FirebaseAuthService implements AuthService {
|
|||
}
|
||||
|
||||
try {
|
||||
await _firebaseAuth.sendPasswordResetEmail(email: email.trim());
|
||||
await auth.sendPasswordResetEmail(email: email.trim());
|
||||
} catch (e) {
|
||||
_updateState(AuthState.error(
|
||||
_formatAuthError(e),
|
||||
|
|
@ -302,8 +315,9 @@ class FirebaseAuthService implements AuthService {
|
|||
|
||||
@override
|
||||
Future<String?> getIdToken([bool forceRefresh = false]) async {
|
||||
if (_firebaseAuth == null) return null;
|
||||
final user = _firebaseAuth.currentUser;
|
||||
final auth = _firebaseAuth;
|
||||
if (auth == null) return null;
|
||||
final user = auth.currentUser;
|
||||
if (user == null) return null;
|
||||
try {
|
||||
return await user.getIdToken(forceRefresh);
|
||||
|
|
@ -325,8 +339,9 @@ class FirebaseAuthService implements AuthService {
|
|||
await _googleSignIn.signOut();
|
||||
} catch (_) {}
|
||||
}
|
||||
if (_firebaseAuth != null) {
|
||||
await _firebaseAuth.signOut();
|
||||
final auth = _firebaseAuth;
|
||||
if (auth != null) {
|
||||
await auth.signOut();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class OpenStreetMapProvider implements AppMapProvider {
|
|||
TileLayer(
|
||||
urlTemplate: config.tileUrlTemplate,
|
||||
subdomains: config.subdomains,
|
||||
userAgentPackageName: 'com.example.motorcycle_dash',
|
||||
userAgentPackageName: 'app.motorsignal.motorcycle',
|
||||
maxZoom: 20,
|
||||
maxNativeZoom: 19,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -80,6 +80,11 @@ class OsmOAuthService extends StateNotifier<OsmAuthState> {
|
|||
static const String _prefUserKey = 'osm_user_profile';
|
||||
static const String _prefVerifierKey = 'osm_pkce_code_verifier';
|
||||
|
||||
String? _currentCodeVerifier;
|
||||
String? _inFlightCode;
|
||||
Future<bool>? _inFlightExchange;
|
||||
String? _lastExchangedCode;
|
||||
|
||||
OsmOAuthService() : super(const OsmAuthState(isLoading: true)) {
|
||||
_loadStoredSession();
|
||||
}
|
||||
|
|
@ -132,6 +137,7 @@ class OsmOAuthService extends StateNotifier<OsmAuthState> {
|
|||
final codeChallenge = _generateCodeChallenge(codeVerifier);
|
||||
final stateParam = _generateRandomString(16);
|
||||
|
||||
_currentCodeVerifier = codeVerifier;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefVerifierKey, codeVerifier);
|
||||
|
||||
|
|
@ -162,17 +168,62 @@ class OsmOAuthService extends StateNotifier<OsmAuthState> {
|
|||
|
||||
/// Complete OAuth2 token exchange with auth code from redirect callback
|
||||
Future<bool> handleAuthCallback(Uri uri) async {
|
||||
final error = uri.queryParameters['error'];
|
||||
if (error != null && error.isNotEmpty) {
|
||||
final desc = uri.queryParameters['error_description'] ?? error;
|
||||
state = state.copyWith(isLoading: false, errorMessage: 'OpenStreetMap authorization error: $desc');
|
||||
return false;
|
||||
}
|
||||
|
||||
final code = uri.queryParameters['code'];
|
||||
if (code == null || code.isEmpty) {
|
||||
state = state.copyWith(errorMessage: 'No authorization code received.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state.isAuthenticated && code == _lastExchangedCode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_inFlightCode == code && _inFlightExchange != null) {
|
||||
return await _inFlightExchange!;
|
||||
}
|
||||
|
||||
_inFlightCode = code;
|
||||
final exchangeFuture = _executeTokenExchange(code);
|
||||
_inFlightExchange = exchangeFuture;
|
||||
|
||||
try {
|
||||
final result = await exchangeFuture;
|
||||
if (result) {
|
||||
_lastExchangedCode = code;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
if (_inFlightCode == code) {
|
||||
_inFlightCode = null;
|
||||
_inFlightExchange = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _executeTokenExchange(String code) async {
|
||||
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final codeVerifier = prefs.getString(_prefVerifierKey) ?? '';
|
||||
var codeVerifier = _currentCodeVerifier;
|
||||
if (codeVerifier == null || codeVerifier.isEmpty) {
|
||||
codeVerifier = prefs.getString(_prefVerifierKey) ?? '';
|
||||
}
|
||||
|
||||
if (codeVerifier.isEmpty) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: 'PKCE code verifier expired or missing. Please try logging in again.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
debugPrint('[OsmOAuth] Exchanging token with code: $code, redirect: $redirectUri');
|
||||
final response = await http.post(
|
||||
|
|
@ -197,6 +248,7 @@ class OsmOAuthService extends StateNotifier<OsmAuthState> {
|
|||
|
||||
await prefs.setString(_prefTokenKey, accessToken);
|
||||
await prefs.remove(_prefVerifierKey);
|
||||
_currentCodeVerifier = null;
|
||||
|
||||
state = OsmAuthState(
|
||||
isAuthenticated: true,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
|
|||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "motorcycle_dash")
|
||||
set(BINARY_NAME "motorsignal")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.motorcycle_dash")
|
||||
set(APPLICATION_ID "app.motorsignal.motorcycle")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:motorcycle_dash/services/alerts/road_alert_models.dart';
|
|||
import 'package:motorcycle_dash/services/alerts/tomtom_road_alert_provider.dart';
|
||||
import 'package:motorcycle_dash/services/alerts/local_road_alert_provider.dart';
|
||||
import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart';
|
||||
import 'package:motorcycle_dash/services/alerts/road_alert_service.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
|
@ -59,5 +60,34 @@ void main() {
|
|||
expect(container.read(tomtomApiKeyProvider), isEmpty);
|
||||
expect(container.read(tomtomStatusProvider), equals(RoadAlertProviderStatus.notConfigured));
|
||||
});
|
||||
|
||||
test('RoadAlertService dismissAlert and clearAllAlerts function correctly', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final service = container.read(roadAlertServiceProvider);
|
||||
|
||||
final future1 = service.alertsStream.first;
|
||||
service.triggerTestAlert(category: RoadAlertCategory.police);
|
||||
final alerts1 = await future1;
|
||||
expect(alerts1.isNotEmpty, isTrue);
|
||||
final alertId = alerts1.first.id;
|
||||
|
||||
final future2 = service.alertsStream.first;
|
||||
service.dismissAlert(alertId);
|
||||
final alerts2 = await future2;
|
||||
expect(alerts2.any((a) => a.id == alertId), isFalse);
|
||||
|
||||
final future3 = service.alertsStream.first;
|
||||
service.triggerTestAlert(category: RoadAlertCategory.hazard);
|
||||
final alerts3 = await future3;
|
||||
expect(alerts3.isNotEmpty, isTrue);
|
||||
|
||||
final future4 = service.alertsStream.first;
|
||||
service.clearAllAlerts();
|
||||
final alerts4 = await future4;
|
||||
expect(alerts4.isEmpty, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue