Remove client secret dependecy and fix session thumbnails
This commit is contained in:
parent
e3837066bb
commit
6e83ce32f8
6 changed files with 136 additions and 119 deletions
|
@ -15,6 +15,7 @@ class ApiClient {
|
||||||
static const String machineIdKey = "machineId";
|
static const String machineIdKey = "machineId";
|
||||||
static const String tokenKey = "token";
|
static const String tokenKey = "token";
|
||||||
static const String passwordKey = "password";
|
static const String passwordKey = "password";
|
||||||
|
static const String uidKey = "uid";
|
||||||
|
|
||||||
ApiClient({required AuthenticationData authenticationData, required this.onLogout})
|
ApiClient({required AuthenticationData authenticationData, required this.onLogout})
|
||||||
: _authenticationData = authenticationData;
|
: _authenticationData = authenticationData;
|
||||||
|
@ -48,12 +49,12 @@ class ApiClient {
|
||||||
"rememberMe": rememberMe,
|
"rememberMe": rememberMe,
|
||||||
"secretMachineId": const Uuid().v4(),
|
"secretMachineId": const Uuid().v4(),
|
||||||
};
|
};
|
||||||
|
final uid = const Uuid().v4().replaceAll("-", "");
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
buildFullUri("/userSessions"),
|
buildFullUri("/userSessions"),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"SecretClientAccessKey": Config.secretClientKey,
|
"UID": uid,
|
||||||
"UID":"2cde2bd72c104d1785af28ae77c29fc2",
|
|
||||||
if (oneTimePad != null) totpKey: oneTimePad,
|
if (oneTimePad != null) totpKey: oneTimePad,
|
||||||
},
|
},
|
||||||
body: jsonEncode(body),
|
body: jsonEncode(body),
|
||||||
|
@ -65,8 +66,9 @@ class ApiClient {
|
||||||
throw "Invalid Credentials";
|
throw "Invalid Credentials";
|
||||||
}
|
}
|
||||||
checkResponseCode(response);
|
checkResponseCode(response);
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
final authData = AuthenticationData.fromMap(jsonDecode(response.body));
|
data["entity"]["uid"] = uid;
|
||||||
|
final authData = AuthenticationData.fromMap(data);
|
||||||
if (authData.isAuthenticated) {
|
if (authData.isAuthenticated) {
|
||||||
const FlutterSecureStorage storage = FlutterSecureStorage(
|
const FlutterSecureStorage storage = FlutterSecureStorage(
|
||||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||||
|
@ -74,6 +76,7 @@ class ApiClient {
|
||||||
await storage.write(key: userIdKey, value: authData.userId);
|
await storage.write(key: userIdKey, value: authData.userId);
|
||||||
await storage.write(key: machineIdKey, value: authData.secretMachineIdHash);
|
await storage.write(key: machineIdKey, value: authData.secretMachineIdHash);
|
||||||
await storage.write(key: tokenKey, value: authData.token);
|
await storage.write(key: tokenKey, value: authData.token);
|
||||||
|
await storage.write(key: uidKey, value: authData.uid);
|
||||||
if (rememberPass) await storage.write(key: passwordKey, value: password);
|
if (rememberPass) await storage.write(key: passwordKey, value: password);
|
||||||
}
|
}
|
||||||
return authData;
|
return authData;
|
||||||
|
@ -87,19 +90,25 @@ class ApiClient {
|
||||||
String? machineId = await storage.read(key: machineIdKey);
|
String? machineId = await storage.read(key: machineIdKey);
|
||||||
String? token = await storage.read(key: tokenKey);
|
String? token = await storage.read(key: tokenKey);
|
||||||
String? password = await storage.read(key: passwordKey);
|
String? password = await storage.read(key: passwordKey);
|
||||||
|
String? uid = await storage.read(key: uidKey);
|
||||||
|
|
||||||
if (userId == null || machineId == null) {
|
if (userId == null || machineId == null || uid == null) {
|
||||||
return AuthenticationData.unauthenticated();
|
return AuthenticationData.unauthenticated();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
final response = await http.patch(buildFullUri("/userSessions"), headers: {
|
final response = await http.patch(buildFullUri("/userSessions"), headers: {
|
||||||
"Authorization": "res $userId:$token",
|
"Authorization": "res $userId:$token",
|
||||||
"UID":"2cde2bd72c104d1785af28ae77c29fc2",
|
"UID": uid,
|
||||||
"SecretClientAccessKey": Config.secretClientKey,
|
|
||||||
});
|
});
|
||||||
if (response.statusCode < 300) {
|
if (response.statusCode < 300) {
|
||||||
return AuthenticationData(userId: userId, token: token, secretMachineIdHash: machineId, isAuthenticated: true);
|
return AuthenticationData(
|
||||||
|
userId: userId,
|
||||||
|
token: token,
|
||||||
|
secretMachineIdHash: machineId,
|
||||||
|
isAuthenticated: true,
|
||||||
|
uid: uid,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ class Config {
|
||||||
static const String apiBaseUrl = "https://api.resonite.com";
|
static const String apiBaseUrl = "https://api.resonite.com";
|
||||||
static const String skyfrostAssetsUrl = "https://assets.resonite.com";
|
static const String skyfrostAssetsUrl = "https://assets.resonite.com";
|
||||||
static const String resoniteHubUrl = "$apiBaseUrl/hub";
|
static const String resoniteHubUrl = "$apiBaseUrl/hub";
|
||||||
static const String secretClientKey = "";
|
|
||||||
|
|
||||||
static const int messageCacheValiditySeconds = 90;
|
static const int messageCacheValiditySeconds = 90;
|
||||||
|
|
||||||
|
|
|
@ -1,40 +1,53 @@
|
||||||
import 'package:contacts_plus_plus/config.dart';
|
import 'package:contacts_plus_plus/config.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class AuthenticationData {
|
class AuthenticationData {
|
||||||
static const _unauthenticated = AuthenticationData(userId: "", token: "", secretMachineIdHash: "", isAuthenticated: false);
|
static const _unauthenticated = AuthenticationData(
|
||||||
|
userId: "",
|
||||||
|
token: "",
|
||||||
|
secretMachineIdHash: "",
|
||||||
|
isAuthenticated: false,
|
||||||
|
uid: "",
|
||||||
|
);
|
||||||
final String userId;
|
final String userId;
|
||||||
final String token;
|
final String token;
|
||||||
final String secretMachineIdHash;
|
final String secretMachineIdHash;
|
||||||
final bool isAuthenticated;
|
final bool isAuthenticated;
|
||||||
|
final String uid;
|
||||||
|
|
||||||
const AuthenticationData({
|
const AuthenticationData({
|
||||||
required this.userId, required this.token, required this.secretMachineIdHash, required this.isAuthenticated
|
required this.userId,
|
||||||
|
required this.token,
|
||||||
|
required this.secretMachineIdHash,
|
||||||
|
required this.isAuthenticated,
|
||||||
|
required this.uid,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AuthenticationData.fromMap(Map map) {
|
factory AuthenticationData.fromMap(Map map) {
|
||||||
map = map["entity"];
|
map = map["entity"];
|
||||||
final userId = map["userId"];
|
final userId = map["userId"];
|
||||||
final token = map["token"];
|
final token = map["token"];
|
||||||
final machineId = map["secretMachineIdHash"];
|
final machineId = map["secretMachineIdHash"];
|
||||||
if (userId == null || token == null || machineId == null) {
|
final uid = map["uid"];
|
||||||
|
if (userId == null || token == null || machineId == null || uid == null) {
|
||||||
return _unauthenticated;
|
return _unauthenticated;
|
||||||
}
|
}
|
||||||
return AuthenticationData(userId: userId, token: token, secretMachineIdHash: machineId, isAuthenticated: true);
|
return AuthenticationData(userId: userId, token: token, secretMachineIdHash: machineId, isAuthenticated: true, uid: uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AuthenticationData.unauthenticated() => _unauthenticated;
|
factory AuthenticationData.unauthenticated() => _unauthenticated;
|
||||||
|
|
||||||
Map<String, String> get authorizationHeader => {
|
Map<String, String> get authorizationHeader => {
|
||||||
"Authorization": "res $userId:$token",
|
"Authorization": "res $userId:$token",
|
||||||
"SecretClientAccessKey": Config.secretClientKey,
|
"UID": uid,
|
||||||
"UID":"2cde2bd72c104d1785af28ae77c29fc2",
|
};
|
||||||
};
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
"userId": userId,
|
"userId": userId,
|
||||||
"token": token,
|
"token": token,
|
||||||
"secretMachineId": secretMachineIdHash,
|
"secretMachineId": secretMachineIdHash,
|
||||||
|
"uid": uid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ class Session {
|
||||||
id: map["sessionId"],
|
id: map["sessionId"],
|
||||||
name: map["name"],
|
name: map["name"],
|
||||||
sessionUsers: (map["sessionUsers"] as List? ?? []).map((entry) => SessionUser.fromMap(entry)).toList(),
|
sessionUsers: (map["sessionUsers"] as List? ?? []).map((entry) => SessionUser.fromMap(entry)).toList(),
|
||||||
thumbnail: map["thumbnail"] ?? "",
|
thumbnail: map["thumbnailUrl"] ?? "",
|
||||||
maxUsers: map["maxUsers"] ?? 0,
|
maxUsers: map["maxUsers"] ?? 0,
|
||||||
hasEnded: map["hasEnded"] ?? false,
|
hasEnded: map["hasEnded"] ?? false,
|
||||||
isValid: map["isValid"] ?? true,
|
isValid: map["isValid"] ?? true,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:contacts_plus_plus/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/authentication_data.dart';
|
import 'package:contacts_plus_plus/models/authentication_data.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:contacts_plus_plus/client_holder.dart';
|
import 'package:contacts_plus_plus/client_holder.dart';
|
||||||
|
@ -73,7 +74,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
await loginSuccessful(authData);
|
await loginSuccessful(authData);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (e == ApiClient.totpKey) {
|
if (e == ApiClient.totpKey) {
|
||||||
if (_needsTotp == false) {
|
if (_needsTotp == false) {
|
||||||
|
@ -81,9 +82,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
_totpFocusNode.requestFocus();
|
_totpFocusNode.requestFocus();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
_scrollController.animateTo(_scrollController.position.maxScrollExtent,
|
_scrollController.animateTo(_scrollController.position.maxScrollExtent,
|
||||||
duration: const Duration(milliseconds: 400),
|
duration: const Duration(milliseconds: 400), curve: Curves.easeOutCirc);
|
||||||
curve: Curves.easeOutCirc
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_error = "The given 2FA code is not valid.";
|
_error = "The given 2FA code is not valid.";
|
||||||
|
@ -92,15 +91,19 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
} else {
|
} else {
|
||||||
_error = "Login unsuccessful: $e.";
|
_error = "Login unsuccessful: $e.";
|
||||||
}
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
FlutterError.reportError(FlutterErrorDetails(
|
||||||
|
exception: e,
|
||||||
|
stack: s,
|
||||||
|
));
|
||||||
|
}
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loginSuccessful(AuthenticationData authData) async {
|
Future<void> loginSuccessful(AuthenticationData authData) async {
|
||||||
final settingsClient = ClientHolder
|
final settingsClient = ClientHolder.of(context).settingsClient;
|
||||||
.of(context)
|
|
||||||
.settingsClient;
|
|
||||||
final notificationManager = FlutterLocalNotificationsPlugin();
|
final notificationManager = FlutterLocalNotificationsPlugin();
|
||||||
if (settingsClient.currentSettings.notificationsDenied.value == null) {
|
if (settingsClient.currentSettings.notificationsDenied.value == null) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
@ -114,19 +117,19 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
await settingsClient.changeSettings(
|
await settingsClient
|
||||||
settingsClient.currentSettings.copyWith(notificationsDenied: true));
|
.changeSettings(settingsClient.currentSettings.copyWith(notificationsDenied: true));
|
||||||
},
|
},
|
||||||
child: const Text("No"),
|
child: const Text("No"),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
final requestResult = await notificationManager.resolvePlatformSpecificImplementation<
|
final requestResult = await notificationManager
|
||||||
AndroidFlutterLocalNotificationsPlugin>()
|
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
|
||||||
?.requestPermission();
|
?.requestPermission();
|
||||||
await settingsClient.changeSettings(settingsClient.currentSettings.copyWith(
|
await settingsClient.changeSettings(settingsClient.currentSettings
|
||||||
notificationsDenied: requestResult == null ? false : !requestResult));
|
.copyWith(notificationsDenied: requestResult == null ? false : !requestResult));
|
||||||
},
|
},
|
||||||
child: const Text("Yes"),
|
child: const Text("Yes"),
|
||||||
)
|
)
|
||||||
|
@ -145,95 +148,84 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Contacts++"),
|
title: const Text("Contacts++"),
|
||||||
),
|
),
|
||||||
body: Builder(
|
body: Builder(builder: (context) {
|
||||||
builder: (context) {
|
return ListView(
|
||||||
return ListView(
|
controller: _scrollController,
|
||||||
controller: _scrollController,
|
children: [
|
||||||
children: [
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.symmetric(vertical: 64),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 64),
|
child: Center(
|
||||||
child: Center(
|
child: Text("Sign In", style: Theme.of(context).textTheme.headlineMedium),
|
||||||
child: Text("Sign In", style: Theme
|
),
|
||||||
.of(context)
|
),
|
||||||
.textTheme
|
Padding(
|
||||||
.headlineMedium),
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
||||||
|
child: TextField(
|
||||||
|
controller: _usernameController,
|
||||||
|
onEditingComplete: () => _passwordFocusNode.requestFocus(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
),
|
||||||
|
labelText: 'Username',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
||||||
|
child: TextField(
|
||||||
|
controller: _passwordController,
|
||||||
|
focusNode: _passwordFocusNode,
|
||||||
|
onEditingComplete: submit,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32)),
|
||||||
|
labelText: 'Password',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_needsTotp)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
||||||
|
child: TextField(
|
||||||
|
controller: _totpController,
|
||||||
|
focusNode: _totpFocusNode,
|
||||||
|
onEditingComplete: submit,
|
||||||
|
obscureText: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
),
|
||||||
|
labelText: '2FA Code',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: TextButton.icon(
|
||||||
|
onPressed: submit,
|
||||||
|
icon: const Icon(Icons.login),
|
||||||
|
label: const Text("Login"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: _errorOpacity,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
||||||
child: TextField(
|
child: Text(_error, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: Colors.red)),
|
||||||
controller: _usernameController,
|
|
||||||
onEditingComplete: () => _passwordFocusNode.requestFocus(),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
),
|
|
||||||
labelText: 'Username',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
)
|
||||||
child: TextField(
|
],
|
||||||
controller: _passwordController,
|
);
|
||||||
focusNode: _passwordFocusNode,
|
}),
|
||||||
onEditingComplete: submit,
|
|
||||||
obscureText: true,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(32)
|
|
||||||
),
|
|
||||||
labelText: 'Password',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_needsTotp)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
|
||||||
child: TextField(
|
|
||||||
controller: _totpController,
|
|
||||||
focusNode: _totpFocusNode,
|
|
||||||
onEditingComplete: submit,
|
|
||||||
obscureText: false,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
),
|
|
||||||
labelText: '2FA Code',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 16),
|
|
||||||
child: _isLoading ?
|
|
||||||
const Center(child: CircularProgressIndicator()) :
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: submit,
|
|
||||||
icon: const Icon(Icons.login),
|
|
||||||
label: const Text("Login"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
opacity: _errorOpacity,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
|
||||||
child: Text(_error, style: Theme
|
|
||||||
.of(context)
|
|
||||||
.textTheme
|
|
||||||
.labelMedium
|
|
||||||
?.copyWith(color: Colors.red)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicato
|
||||||
import 'package:contacts_plus_plus/widgets/messages/message_input_bar.dart';
|
import 'package:contacts_plus_plus/widgets/messages/message_input_bar.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart';
|
import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'message_bubble.dart';
|
import 'message_bubble.dart';
|
||||||
|
@ -60,6 +61,9 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor: Theme.of(context).colorScheme.background,
|
||||||
|
),
|
||||||
title: Row(
|
title: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
Loading…
Reference in a new issue