CHECKPOINT: Working signed release build with Google authentication
This commit is contained in:
parent
78d3d45570
commit
81064da7f4
|
|
@ -54,3 +54,9 @@ node_modules/
|
||||||
scratch/
|
scratch/
|
||||||
flutter-doctor.txt
|
flutter-doctor.txt
|
||||||
gemini-mcp-config-search.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" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="motorcycle_dash"
|
android:label="MotorSignal"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/launcher_icon">
|
android:icon="@mipmap/launcher_icon">
|
||||||
<activity
|
<activity
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.example.motorcycle_dash
|
package app.motorsignal.motorcycle
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
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
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# This newDsl flag was added by the Flutter template
|
|
||||||
android.newDsl=false
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
# This builtInKotlin flag was added by the Flutter template
|
|
||||||
android.builtInKotlin=false
|
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("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "9.0.1" apply false
|
id("com.android.application") version "9.0.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.3.20" 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")
|
include(":app")
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class DashboardScreen extends ConsumerWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (!hasLocPermission) _buildPermissionBanner(context, ref),
|
if (!hasLocPermission) _buildPermissionBanner(context, ref),
|
||||||
if (closestAlert != null) _buildAlertBanner(closestAlert),
|
if (closestAlert != null) _buildAlertBanner(context, closestAlert, ref),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
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(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: alert.category.color.withValues(alpha: 0.2),
|
color: alert.category.color.withValues(alpha: 0.2),
|
||||||
border: Border(bottom: BorderSide(color: alert.category.color, width: 2)),
|
border: Border(bottom: BorderSide(color: alert.category.color, width: 2)),
|
||||||
|
|
@ -120,15 +120,36 @@ class DashboardScreen extends ConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
alert.category.displayName.toUpperCase(),
|
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(
|
Text(
|
||||||
'${alert.distanceFromUser?.toStringAsFixed(0) ?? '?'}m Ahead',
|
'${alert.distanceFromUser?.toStringAsFixed(0) ?? '?'}m Ahead • ${alert.roadName ?? 'Reported'}',
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
|
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> {
|
class _OAuthCallbackScreenState extends ConsumerState<OAuthCallbackScreen> {
|
||||||
String _status = 'Connecting to OpenStreetMap...';
|
String _status = 'Connecting to OpenStreetMap...';
|
||||||
bool _isSuccess = false;
|
bool _isSuccess = false;
|
||||||
|
bool _hasProcessed = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_processAuth();
|
if (!_hasProcessed) {
|
||||||
|
_hasProcessed = true;
|
||||||
|
_processAuth();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -289,10 +289,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Expanded(
|
||||||
'OpenStreetMap Account',
|
child: Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
|
'OpenStreetMap Account',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Chip(
|
Chip(
|
||||||
label: Text(
|
label: Text(
|
||||||
isAuthed ? 'Connected' : 'Disconnected',
|
isAuthed ? 'Connected' : 'Disconnected',
|
||||||
|
|
|
||||||
|
|
@ -106,24 +106,32 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
const Expanded(
|
||||||
children: [
|
child: Row(
|
||||||
Icon(Icons.palette_outlined, color: Color(0xFF00E5FF), size: 18),
|
children: [
|
||||||
SizedBox(width: 8),
|
Icon(Icons.palette_outlined, color: Color(0xFF00E5FF), size: 18),
|
||||||
Text(
|
SizedBox(width: 8),
|
||||||
'ACTIVE THEME:',
|
Flexible(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontFamily: 'Inter',
|
'ACTIVE THEME:',
|
||||||
fontSize: 11,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.w800,
|
style: TextStyle(
|
||||||
color: Colors.white70,
|
fontFamily: 'Inter',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
SegmentedButton<DashboardThemeMode>(
|
SegmentedButton<DashboardThemeMode>(
|
||||||
|
showSelectedIcon: false,
|
||||||
style: const ButtonStyle(
|
style: const ButtonStyle(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
),
|
),
|
||||||
segments: const [
|
segments: const [
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
|
|
@ -268,22 +276,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
const Expanded(
|
||||||
children: [
|
child: Row(
|
||||||
Icon(Icons.account_circle, color: Color(0xFF00E5FF), size: 18),
|
children: [
|
||||||
SizedBox(width: 8),
|
Icon(Icons.account_circle, color: Color(0xFF00E5FF), size: 18),
|
||||||
Text(
|
SizedBox(width: 8),
|
||||||
'MOTORCLOUD ACCOUNT',
|
Flexible(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontFamily: 'Sora',
|
'MOTORCLOUD ACCOUNT',
|
||||||
fontSize: 12,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.w800,
|
style: TextStyle(
|
||||||
color: Color(0xFF00E5FF),
|
fontFamily: 'Sora',
|
||||||
letterSpacing: 0.1,
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Color(0xFF00E5FF),
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -398,22 +412,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
const Expanded(
|
||||||
children: [
|
child: Row(
|
||||||
Icon(Icons.public, color: Color(0xFF00E5FF), size: 18),
|
children: [
|
||||||
SizedBox(width: 8),
|
Icon(Icons.public, color: Color(0xFF00E5FF), size: 18),
|
||||||
Text(
|
SizedBox(width: 8),
|
||||||
'OPENSTREETMAP ACCOUNT',
|
Flexible(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontFamily: 'Sora',
|
'OPENSTREETMAP ACCOUNT',
|
||||||
fontSize: 12,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.w800,
|
style: TextStyle(
|
||||||
color: Color(0xFF00E5FF),
|
fontFamily: 'Sora',
|
||||||
letterSpacing: 0.1,
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Color(0xFF00E5FF),
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -775,14 +795,17 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.screen_rotation, color: Color(0xFF00E5FF), size: 18),
|
Icon(Icons.screen_rotation, color: Color(0xFF00E5FF), size: 18),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Expanded(
|
||||||
'SENSORS & MOUNT CALIBRATION',
|
child: Text(
|
||||||
style: TextStyle(
|
'SENSORS & MOUNT CALIBRATION',
|
||||||
fontFamily: 'Inter',
|
overflow: TextOverflow.ellipsis,
|
||||||
fontSize: 12,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w800,
|
fontFamily: 'Inter',
|
||||||
color: Colors.white,
|
fontSize: 12,
|
||||||
letterSpacing: 0.1,
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -983,22 +1006,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Expanded(
|
||||||
children: [
|
child: Row(
|
||||||
Icon(Icons.biotech, color: isEnabled ? Colors.greenAccent : const Color(0xFF00E5FF), size: 18),
|
children: [
|
||||||
const SizedBox(width: 8),
|
Icon(Icons.biotech, color: isEnabled ? Colors.greenAccent : const Color(0xFF00E5FF), size: 18),
|
||||||
const Text(
|
const SizedBox(width: 8),
|
||||||
'ENGINEERING & DIAGNOSTICS',
|
const Flexible(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontFamily: 'Inter',
|
'ENGINEERING & DIAGNOSTICS',
|
||||||
fontSize: 12,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.w800,
|
style: TextStyle(
|
||||||
color: Colors.white,
|
fontFamily: 'Inter',
|
||||||
letterSpacing: 0.1,
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Switch(
|
Switch(
|
||||||
value: isEnabled,
|
value: isEnabled,
|
||||||
activeThumbColor: Colors.greenAccent,
|
activeThumbColor: Colors.greenAccent,
|
||||||
|
|
@ -1118,22 +1147,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
const Expanded(
|
||||||
children: [
|
child: Row(
|
||||||
Icon(Icons.cloud_upload_outlined, color: Color(0xFF00E5FF), size: 18),
|
children: [
|
||||||
SizedBox(width: 8),
|
Icon(Icons.cloud_upload_outlined, color: Color(0xFF00E5FF), size: 18),
|
||||||
Text(
|
SizedBox(width: 8),
|
||||||
'GITHUB RIDE LOG SYNC',
|
Flexible(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontFamily: 'Inter',
|
'GITHUB RIDE LOG SYNC',
|
||||||
fontSize: 12,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.w800,
|
style: TextStyle(
|
||||||
color: Colors.white,
|
fontFamily: 'Inter',
|
||||||
letterSpacing: 0.1,
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -1255,22 +1290,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
const Expanded(
|
||||||
children: [
|
child: Row(
|
||||||
Icon(Icons.traffic, color: Color(0xFF00E5FF), size: 18),
|
children: [
|
||||||
SizedBox(width: 8),
|
Icon(Icons.traffic, color: Color(0xFF00E5FF), size: 18),
|
||||||
Text(
|
SizedBox(width: 8),
|
||||||
'TOMTOM TRAFFIC PROVIDER',
|
Flexible(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontFamily: 'Inter',
|
'TOMTOM TRAFFIC PROVIDER',
|
||||||
fontSize: 12,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.w800,
|
style: TextStyle(
|
||||||
color: Colors.white,
|
fontFamily: 'Inter',
|
||||||
letterSpacing: 0.1,
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
_buildStatusBadge(tomtomStatus),
|
_buildStatusBadge(tomtomStatus),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -1352,22 +1393,28 @@ class _EvolutionSettingsViewState extends ConsumerState<EvolutionSettingsView> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
const Expanded(
|
||||||
children: [
|
child: Row(
|
||||||
Icon(Icons.local_police, color: Color(0xFF00E5FF), size: 18),
|
children: [
|
||||||
SizedBox(width: 8),
|
Icon(Icons.local_police, color: Color(0xFF00E5FF), size: 18),
|
||||||
Text(
|
SizedBox(width: 8),
|
||||||
'COMMUNITY TRAFFIC PROVIDER',
|
Flexible(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontFamily: 'Inter',
|
'COMMUNITY TRAFFIC PROVIDER',
|
||||||
fontSize: 12,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.w800,
|
style: TextStyle(
|
||||||
color: Colors.white,
|
fontFamily: 'Inter',
|
||||||
letterSpacing: 0.1,
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
_buildStatusBadge(communityStatus),
|
_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:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'firebase_options.dart';
|
||||||
import 'core/theme/theme.dart';
|
import 'core/theme/theme.dart';
|
||||||
import 'core/router.dart';
|
import 'core/router.dart';
|
||||||
import 'features/records/records_service.dart';
|
import 'features/records/records_service.dart';
|
||||||
import 'services/osm_oauth_service.dart';
|
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
// Optional Firebase initialization (gracefully defaults to standalone offline mode if unconfigured)
|
// Authoritative Firebase initialization
|
||||||
try {
|
try {
|
||||||
await Firebase.initializeApp();
|
await Firebase.initializeApp(
|
||||||
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[Firebase] Standalone offline mode active: $e');
|
debugPrint('[Firebase] Standalone offline mode active: $e');
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +69,6 @@ class _MotorcycleDashAppState extends ConsumerState<MotorcycleDashApp> {
|
||||||
void _handleDeepLink(Uri uri) {
|
void _handleDeepLink(Uri uri) {
|
||||||
final uriStr = uri.toString();
|
final uriStr = uri.toString();
|
||||||
if (uriStr.contains('callback') || uri.host == 'oauth' || uri.scheme == 'motorsignal') {
|
if (uriStr.contains('callback') || uri.host == 'oauth' || uri.scheme == 'motorsignal') {
|
||||||
ref.read(osmOAuthServiceProvider.notifier).handleAuthCallback(uri);
|
|
||||||
try {
|
try {
|
||||||
appRouter.go('/oauth/callback?${uri.query}');
|
appRouter.go('/oauth/callback?${uri.query}');
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,17 @@ class RoadAlertService {
|
||||||
|
|
||||||
List<RoadAlert> _cachedAlerts = [];
|
List<RoadAlert> _cachedAlerts = [];
|
||||||
final Set<String> _announcedAlertIds = {};
|
final Set<String> _announcedAlertIds = {};
|
||||||
|
final Set<String> _dismissedAlertIds = {};
|
||||||
|
Timer? _cleanupTimer;
|
||||||
|
|
||||||
Stream<List<RoadAlert>> get alertsStream => _alertsController.stream;
|
Stream<List<RoadAlert>> get alertsStream => _alertsController.stream;
|
||||||
|
|
||||||
RoadAlertService(this._ref) {
|
RoadAlertService(this._ref) {
|
||||||
_initTts();
|
_initTts();
|
||||||
_startListening();
|
_startListening();
|
||||||
|
_cleanupTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||||||
|
_pruneExpiredAlerts();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initTts() {
|
void _initTts() {
|
||||||
|
|
@ -308,7 +313,7 @@ class RoadAlertService {
|
||||||
description: 'TEST ALERT: ${category.displayName} reported ahead (Diagnostic Verification)',
|
description: 'TEST ALERT: ${category.displayName} reported ahead (Diagnostic Verification)',
|
||||||
roadName: 'Test Way / Simulation',
|
roadName: 'Test Way / Simulation',
|
||||||
reportedAt: now,
|
reportedAt: now,
|
||||||
expiresAt: now.add(const Duration(minutes: 5)),
|
expiresAt: now.add(const Duration(seconds: 12)),
|
||||||
lastUpdated: now,
|
lastUpdated: now,
|
||||||
distanceFromUser: 150.0,
|
distanceFromUser: 150.0,
|
||||||
source: 'Internal Pipeline Test',
|
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) {
|
void simulateAlert(RoadAlert alert) {
|
||||||
if (!_ref.read(roadAlertsEnabledProvider)) return;
|
if (!_ref.read(roadAlertsEnabledProvider)) return;
|
||||||
_cachedAlerts = [alert, ..._cachedAlerts];
|
_cachedAlerts = [alert, ..._cachedAlerts];
|
||||||
|
|
@ -350,6 +399,7 @@ class RoadAlertService {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
debugPrint('[RoadAlerts] Disposing service resources');
|
debugPrint('[RoadAlerts] Disposing service resources');
|
||||||
}
|
}
|
||||||
|
_cleanupTimer?.cancel();
|
||||||
_locationSub?.cancel();
|
_locationSub?.cancel();
|
||||||
_alertsController.close();
|
_alertsController.close();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ abstract class AuthService {
|
||||||
|
|
||||||
/// Authoritative Firebase Authentication Service
|
/// Authoritative Firebase Authentication Service
|
||||||
class FirebaseAuthService implements AuthService {
|
class FirebaseAuthService implements AuthService {
|
||||||
final fb.FirebaseAuth? _firebaseAuth;
|
fb.FirebaseAuth? _firebaseAuth;
|
||||||
final GoogleSignIn? _googleSignIn;
|
final GoogleSignIn? _googleSignIn;
|
||||||
|
|
||||||
final _stateController = StreamController<AuthState>.broadcast();
|
final _stateController = StreamController<AuthState>.broadcast();
|
||||||
|
|
@ -60,10 +60,18 @@ class FirebaseAuthService implements AuthService {
|
||||||
AuthUser? get currentUser => _currentState.user;
|
AuthUser? get currentUser => _currentState.user;
|
||||||
|
|
||||||
@override
|
@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() {
|
void _init() {
|
||||||
if (_firebaseAuth == null) {
|
final auth = _firebaseAuth;
|
||||||
|
if (auth == null) {
|
||||||
// Firebase is not configured or offline during startup
|
// Firebase is not configured or offline during startup
|
||||||
_updateState(const AuthState.unauthenticated(isFirebaseConfigured: false));
|
_updateState(const AuthState.unauthenticated(isFirebaseConfigured: false));
|
||||||
return;
|
return;
|
||||||
|
|
@ -71,7 +79,7 @@ class FirebaseAuthService implements AuthService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Listen to native Firebase auth state stream
|
// Listen to native Firebase auth state stream
|
||||||
_fbAuthSubscription = _firebaseAuth.authStateChanges().listen(
|
_fbAuthSubscription = auth.authStateChanges().listen(
|
||||||
(fb.User? user) {
|
(fb.User? user) {
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
_updateState(const AuthState.unauthenticated(isFirebaseConfigured: true));
|
_updateState(const AuthState.unauthenticated(isFirebaseConfigured: true));
|
||||||
|
|
@ -125,7 +133,8 @@ class FirebaseAuthService implements AuthService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> signInWithEmailAndPassword(String email, String password) async {
|
Future<void> signInWithEmailAndPassword(String email, String password) async {
|
||||||
if (!isConfigured || _firebaseAuth == null) {
|
final auth = _firebaseAuth;
|
||||||
|
if (!isConfigured || auth == null) {
|
||||||
_updateState(AuthState.error(
|
_updateState(AuthState.error(
|
||||||
'Cloud authentication is unavailable in offline/unconfigured mode. Local ride recording is active.',
|
'Cloud authentication is unavailable in offline/unconfigured mode. Local ride recording is active.',
|
||||||
user: _currentState.user,
|
user: _currentState.user,
|
||||||
|
|
@ -140,7 +149,7 @@ class FirebaseAuthService implements AuthService {
|
||||||
));
|
));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final credential = await _firebaseAuth.signInWithEmailAndPassword(
|
final credential = await auth.signInWithEmailAndPassword(
|
||||||
email: email.trim(),
|
email: email.trim(),
|
||||||
password: password,
|
password: password,
|
||||||
);
|
);
|
||||||
|
|
@ -163,7 +172,8 @@ class FirebaseAuthService implements AuthService {
|
||||||
String password, {
|
String password, {
|
||||||
String? displayName,
|
String? displayName,
|
||||||
}) async {
|
}) async {
|
||||||
if (!isConfigured || _firebaseAuth == null) {
|
final auth = _firebaseAuth;
|
||||||
|
if (!isConfigured || auth == null) {
|
||||||
_updateState(AuthState.error(
|
_updateState(AuthState.error(
|
||||||
'Account registration requires cloud configuration. Local riding remains fully functional.',
|
'Account registration requires cloud configuration. Local riding remains fully functional.',
|
||||||
user: _currentState.user,
|
user: _currentState.user,
|
||||||
|
|
@ -178,7 +188,7 @@ class FirebaseAuthService implements AuthService {
|
||||||
));
|
));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final credential = await _firebaseAuth.createUserWithEmailAndPassword(
|
final credential = await auth.createUserWithEmailAndPassword(
|
||||||
email: email.trim(),
|
email: email.trim(),
|
||||||
password: password,
|
password: password,
|
||||||
);
|
);
|
||||||
|
|
@ -200,7 +210,8 @@ class FirebaseAuthService implements AuthService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> signInWithGoogle() async {
|
Future<void> signInWithGoogle() async {
|
||||||
if (!isConfigured || _firebaseAuth == null) {
|
final auth = _firebaseAuth;
|
||||||
|
if (!isConfigured || auth == null) {
|
||||||
_updateState(AuthState.error(
|
_updateState(AuthState.error(
|
||||||
'Google Sign-In is unavailable without Firebase credentials. Local ride features remain available.',
|
'Google Sign-In is unavailable without Firebase credentials. Local ride features remain available.',
|
||||||
user: _currentState.user,
|
user: _currentState.user,
|
||||||
|
|
@ -230,7 +241,7 @@ class FirebaseAuthService implements AuthService {
|
||||||
idToken: googleAuth.idToken,
|
idToken: googleAuth.idToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
final result = await _firebaseAuth.signInWithCredential(credential);
|
final result = await auth.signInWithCredential(credential);
|
||||||
if (result.user != null) {
|
if (result.user != null) {
|
||||||
final user = _mapFirebaseUser(result.user!);
|
final user = _mapFirebaseUser(result.user!);
|
||||||
_updateState(AuthState.authenticated(user, isFirebaseConfigured: true));
|
_updateState(AuthState.authenticated(user, isFirebaseConfigured: true));
|
||||||
|
|
@ -246,7 +257,8 @@ class FirebaseAuthService implements AuthService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> signInAnonymously() async {
|
Future<void> signInAnonymously() async {
|
||||||
if (!isConfigured || _firebaseAuth == null) {
|
final auth = _firebaseAuth;
|
||||||
|
if (!isConfigured || auth == null) {
|
||||||
// Local fallback anonymous user representation
|
// Local fallback anonymous user representation
|
||||||
final localAnon = AuthUser(
|
final localAnon = AuthUser(
|
||||||
uid: 'local_${DateTime.now().millisecondsSinceEpoch}',
|
uid: 'local_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
|
@ -264,7 +276,7 @@ class FirebaseAuthService implements AuthService {
|
||||||
));
|
));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final credential = await _firebaseAuth.signInAnonymously();
|
final credential = await auth.signInAnonymously();
|
||||||
if (credential.user != null) {
|
if (credential.user != null) {
|
||||||
final user = _mapFirebaseUser(credential.user!);
|
final user = _mapFirebaseUser(credential.user!);
|
||||||
_updateState(AuthState.anonymous(user, isFirebaseConfigured: true));
|
_updateState(AuthState.anonymous(user, isFirebaseConfigured: true));
|
||||||
|
|
@ -280,7 +292,8 @@ class FirebaseAuthService implements AuthService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> sendPasswordResetEmail(String email) async {
|
Future<void> sendPasswordResetEmail(String email) async {
|
||||||
if (!isConfigured || _firebaseAuth == null) {
|
final auth = _firebaseAuth;
|
||||||
|
if (!isConfigured || auth == null) {
|
||||||
_updateState(AuthState.error(
|
_updateState(AuthState.error(
|
||||||
'Password reset is unavailable in offline mode.',
|
'Password reset is unavailable in offline mode.',
|
||||||
user: _currentState.user,
|
user: _currentState.user,
|
||||||
|
|
@ -290,7 +303,7 @@ class FirebaseAuthService implements AuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _firebaseAuth.sendPasswordResetEmail(email: email.trim());
|
await auth.sendPasswordResetEmail(email: email.trim());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_updateState(AuthState.error(
|
_updateState(AuthState.error(
|
||||||
_formatAuthError(e),
|
_formatAuthError(e),
|
||||||
|
|
@ -302,8 +315,9 @@ class FirebaseAuthService implements AuthService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> getIdToken([bool forceRefresh = false]) async {
|
Future<String?> getIdToken([bool forceRefresh = false]) async {
|
||||||
if (_firebaseAuth == null) return null;
|
final auth = _firebaseAuth;
|
||||||
final user = _firebaseAuth.currentUser;
|
if (auth == null) return null;
|
||||||
|
final user = auth.currentUser;
|
||||||
if (user == null) return null;
|
if (user == null) return null;
|
||||||
try {
|
try {
|
||||||
return await user.getIdToken(forceRefresh);
|
return await user.getIdToken(forceRefresh);
|
||||||
|
|
@ -325,8 +339,9 @@ class FirebaseAuthService implements AuthService {
|
||||||
await _googleSignIn.signOut();
|
await _googleSignIn.signOut();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (_firebaseAuth != null) {
|
final auth = _firebaseAuth;
|
||||||
await _firebaseAuth.signOut();
|
if (auth != null) {
|
||||||
|
await auth.signOut();
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ class OpenStreetMapProvider implements AppMapProvider {
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: config.tileUrlTemplate,
|
urlTemplate: config.tileUrlTemplate,
|
||||||
subdomains: config.subdomains,
|
subdomains: config.subdomains,
|
||||||
userAgentPackageName: 'com.example.motorcycle_dash',
|
userAgentPackageName: 'app.motorsignal.motorcycle',
|
||||||
maxZoom: 20,
|
maxZoom: 20,
|
||||||
maxNativeZoom: 19,
|
maxNativeZoom: 19,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,11 @@ class OsmOAuthService extends StateNotifier<OsmAuthState> {
|
||||||
static const String _prefUserKey = 'osm_user_profile';
|
static const String _prefUserKey = 'osm_user_profile';
|
||||||
static const String _prefVerifierKey = 'osm_pkce_code_verifier';
|
static const String _prefVerifierKey = 'osm_pkce_code_verifier';
|
||||||
|
|
||||||
|
String? _currentCodeVerifier;
|
||||||
|
String? _inFlightCode;
|
||||||
|
Future<bool>? _inFlightExchange;
|
||||||
|
String? _lastExchangedCode;
|
||||||
|
|
||||||
OsmOAuthService() : super(const OsmAuthState(isLoading: true)) {
|
OsmOAuthService() : super(const OsmAuthState(isLoading: true)) {
|
||||||
_loadStoredSession();
|
_loadStoredSession();
|
||||||
}
|
}
|
||||||
|
|
@ -132,6 +137,7 @@ class OsmOAuthService extends StateNotifier<OsmAuthState> {
|
||||||
final codeChallenge = _generateCodeChallenge(codeVerifier);
|
final codeChallenge = _generateCodeChallenge(codeVerifier);
|
||||||
final stateParam = _generateRandomString(16);
|
final stateParam = _generateRandomString(16);
|
||||||
|
|
||||||
|
_currentCodeVerifier = codeVerifier;
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString(_prefVerifierKey, codeVerifier);
|
await prefs.setString(_prefVerifierKey, codeVerifier);
|
||||||
|
|
||||||
|
|
@ -162,17 +168,62 @@ class OsmOAuthService extends StateNotifier<OsmAuthState> {
|
||||||
|
|
||||||
/// Complete OAuth2 token exchange with auth code from redirect callback
|
/// Complete OAuth2 token exchange with auth code from redirect callback
|
||||||
Future<bool> handleAuthCallback(Uri uri) async {
|
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'];
|
final code = uri.queryParameters['code'];
|
||||||
if (code == null || code.isEmpty) {
|
if (code == null || code.isEmpty) {
|
||||||
state = state.copyWith(errorMessage: 'No authorization code received.');
|
state = state.copyWith(errorMessage: 'No authorization code received.');
|
||||||
return false;
|
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);
|
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
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');
|
debugPrint('[OsmOAuth] Exchanging token with code: $code, redirect: $redirectUri');
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
|
|
@ -197,6 +248,7 @@ class OsmOAuthService extends StateNotifier<OsmAuthState> {
|
||||||
|
|
||||||
await prefs.setString(_prefTokenKey, accessToken);
|
await prefs.setString(_prefTokenKey, accessToken);
|
||||||
await prefs.remove(_prefVerifierKey);
|
await prefs.remove(_prefVerifierKey);
|
||||||
|
_currentCodeVerifier = null;
|
||||||
|
|
||||||
state = OsmAuthState(
|
state = OsmAuthState(
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
# The name of the executable created for the application. Change this to change
|
# The name of the executable created for the application. Change this to change
|
||||||
# the on-disk name of your application.
|
# 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:
|
# The unique GTK application identifier for this application. See:
|
||||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
# 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
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
# versions of CMake.
|
# 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/tomtom_road_alert_provider.dart';
|
||||||
import 'package:motorcycle_dash/services/alerts/local_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_providers.dart';
|
||||||
|
import 'package:motorcycle_dash/services/alerts/road_alert_service.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
@ -59,5 +60,34 @@ void main() {
|
||||||
expect(container.read(tomtomApiKeyProvider), isEmpty);
|
expect(container.read(tomtomApiKeyProvider), isEmpty);
|
||||||
expect(container.read(tomtomStatusProvider), equals(RoadAlertProviderStatus.notConfigured));
|
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