login finish
This commit is contained in:
parent
77e94b5ec0
commit
b62d4c56ea
@ -2,5 +2,5 @@ class AppConfig {
|
||||
const AppConfig._();
|
||||
|
||||
static const String apiBaseUrl = 'https://api-tms-uat.kbzbank.com:8443';
|
||||
static const String eReceiptSecret = 'y812J21lhha11OS';
|
||||
static const String apiSecret = 'y812J21lhha11OS';
|
||||
}
|
||||
|
||||
93
lib/core/network/auth_http_client.dart
Normal file
93
lib/core/network/auth_http_client.dart
Normal file
@ -0,0 +1,93 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
typedef AccessTokenProvider = String? Function();
|
||||
typedef RefreshAccessToken = Future<String?> Function();
|
||||
typedef RequestUriPredicate = bool Function(Uri uri);
|
||||
|
||||
class AuthHttpClient extends http.BaseClient {
|
||||
AuthHttpClient({
|
||||
required http.Client innerClient,
|
||||
required AccessTokenProvider accessTokenProvider,
|
||||
required RefreshAccessToken refreshAccessToken,
|
||||
RequestUriPredicate? shouldAttachToken,
|
||||
RequestUriPredicate? shouldRefreshOnUnauthorized,
|
||||
}) : _innerClient = innerClient,
|
||||
_accessTokenProvider = accessTokenProvider,
|
||||
_refreshAccessToken = refreshAccessToken,
|
||||
_shouldAttachToken = shouldAttachToken ?? _always,
|
||||
_shouldRefreshOnUnauthorized =
|
||||
shouldRefreshOnUnauthorized ?? _always;
|
||||
|
||||
final http.Client _innerClient;
|
||||
final AccessTokenProvider _accessTokenProvider;
|
||||
final RefreshAccessToken _refreshAccessToken;
|
||||
final RequestUriPredicate _shouldAttachToken;
|
||||
final RequestUriPredicate _shouldRefreshOnUnauthorized;
|
||||
|
||||
Future<String?>? _ongoingRefresh;
|
||||
|
||||
static bool _always(Uri _) => true;
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
if (_shouldAttachToken(request.url)) {
|
||||
final token = _accessTokenProvider();
|
||||
if (token != null &&
|
||||
token.trim().isNotEmpty &&
|
||||
!request.headers.containsKey('Authorization')) {
|
||||
request.headers['Authorization'] = 'Bearer ${token.trim()}';
|
||||
}
|
||||
}
|
||||
|
||||
final firstResponse = await _innerClient.send(request);
|
||||
if (firstResponse.statusCode != 401 ||
|
||||
!_shouldRefreshOnUnauthorized(request.url) ||
|
||||
request is! http.Request) {
|
||||
return firstResponse;
|
||||
}
|
||||
|
||||
final refreshedToken = await _refreshTokenWithLock();
|
||||
if (refreshedToken == null || refreshedToken.trim().isEmpty) {
|
||||
return firstResponse;
|
||||
}
|
||||
|
||||
await firstResponse.stream.drain();
|
||||
final retryRequest = _cloneRequest(request);
|
||||
retryRequest.headers['Authorization'] = 'Bearer ${refreshedToken.trim()}';
|
||||
return _innerClient.send(retryRequest);
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
_innerClient.close();
|
||||
super.close();
|
||||
}
|
||||
|
||||
Future<String?> _refreshTokenWithLock() async {
|
||||
final inFlight = _ongoingRefresh;
|
||||
if (inFlight != null) {
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
final nextRefresh = _refreshAccessToken();
|
||||
_ongoingRefresh = nextRefresh;
|
||||
try {
|
||||
return await nextRefresh;
|
||||
} finally {
|
||||
_ongoingRefresh = null;
|
||||
}
|
||||
}
|
||||
|
||||
http.Request _cloneRequest(http.Request original) {
|
||||
final cloned = http.Request(original.method, original.url)
|
||||
..followRedirects = original.followRedirects
|
||||
..maxRedirects = original.maxRedirects
|
||||
..persistentConnection = original.persistentConnection
|
||||
..encoding = original.encoding
|
||||
..bodyBytes = original.bodyBytes;
|
||||
cloned.headers.addAll(original.headers);
|
||||
return cloned;
|
||||
}
|
||||
}
|
||||
@ -48,7 +48,7 @@ class LoggingHttpClient extends http.BaseClient {
|
||||
void _logRequest(http.BaseRequest request, String? requestBody) {
|
||||
final safeHeaders = _sanitizeHeaders(request.headers);
|
||||
debugPrint('[API][REQUEST] ${request.method} ${request.url}');
|
||||
debugPrint('[API][REQUEST][HEADERS] ${request.headers}');
|
||||
debugPrint('[API][REQUEST][HEADERS] $safeHeaders');
|
||||
if (requestBody != null && requestBody.isNotEmpty) {
|
||||
debugPrint('[API][REQUEST][BODY] $requestBody');
|
||||
}
|
||||
|
||||
@ -2,18 +2,17 @@ import 'dart:convert';
|
||||
|
||||
import 'package:e_receipt_mobile/domain/entities/login_user.dart';
|
||||
import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class ApiAuthRepository implements AuthRepository {
|
||||
ApiAuthRepository({
|
||||
required this.baseUrl,
|
||||
required this.eReceiptSecret,
|
||||
required this.apiSecret,
|
||||
http.Client? client,
|
||||
}) : _client = client ?? http.Client();
|
||||
|
||||
final String baseUrl;
|
||||
final String eReceiptSecret;
|
||||
final String apiSecret;
|
||||
final http.Client _client;
|
||||
|
||||
@override
|
||||
@ -26,33 +25,56 @@ class ApiAuthRepository implements AuthRepository {
|
||||
'username': username.trim().toLowerCase(),
|
||||
'password': password,
|
||||
});
|
||||
final encodedApiKey = _generateApiKey(requestBody);
|
||||
|
||||
final response = await _client.post(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': encodedApiKey,
|
||||
'x-api-key': apiSecret,
|
||||
},
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final data = _asMap(response.body);
|
||||
final apiUsername = (data['username'] ?? data['email'])?.toString();
|
||||
return LoginUser(
|
||||
username: apiUsername?.trim().isNotEmpty == true
|
||||
? apiUsername!.trim()
|
||||
: username.trim(),
|
||||
return _parseUserFromResponse(
|
||||
response.body,
|
||||
fallbackUsername: username.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
throw Exception(_extractErrorMessage(response.body));
|
||||
}
|
||||
|
||||
String _generateApiKey(String bodyString) {
|
||||
final dataToHash = '$bodyString$eReceiptSecret';
|
||||
return sha256.convert(utf8.encode(dataToHash)).toString();
|
||||
@override
|
||||
Future<LoginUser> refreshSession({
|
||||
required String username,
|
||||
required String refreshToken,
|
||||
required String role,
|
||||
}) async {
|
||||
final uri = Uri.parse('$baseUrl/receipt/auth/refresh-token');
|
||||
final requestBody = jsonEncode({
|
||||
'refreshToken': refreshToken,
|
||||
});
|
||||
|
||||
final response = await _client.post(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiSecret,
|
||||
},
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return _parseUserFromResponse(
|
||||
response.body,
|
||||
fallbackUsername: username,
|
||||
fallbackRefreshToken: refreshToken,
|
||||
fallbackRole: role,
|
||||
);
|
||||
}
|
||||
|
||||
throw Exception(_extractErrorMessage(response.body));
|
||||
}
|
||||
|
||||
Map<String, dynamic> _asMap(String body) {
|
||||
@ -75,4 +97,62 @@ class ApiAuthRepository implements AuthRepository {
|
||||
}
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
LoginUser _parseUserFromResponse(
|
||||
String body, {
|
||||
required String fallbackUsername,
|
||||
String? fallbackRefreshToken,
|
||||
String? fallbackRole,
|
||||
}) {
|
||||
final data = _asMap(body);
|
||||
final payload = _extractPayload(data);
|
||||
final apiUsername = _pickString(payload, const ['username', 'email']);
|
||||
final token = _pickString(payload, const ['token', 'accessToken']);
|
||||
final refreshToken = _pickString(
|
||||
payload,
|
||||
const ['refreshToken', 'refresh_token'],
|
||||
);
|
||||
final role = _pickString(payload, const ['role']);
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Auth response is missing token');
|
||||
}
|
||||
|
||||
final effectiveRefreshToken = refreshToken ?? fallbackRefreshToken;
|
||||
if (effectiveRefreshToken == null) {
|
||||
throw Exception('Auth response is missing refresh token');
|
||||
}
|
||||
|
||||
final effectiveRole = role ?? fallbackRole;
|
||||
if (effectiveRole == null || effectiveRole.trim().isEmpty) {
|
||||
throw Exception('Auth response is missing role');
|
||||
}
|
||||
|
||||
return LoginUser(
|
||||
username: apiUsername?.trim().isNotEmpty == true
|
||||
? apiUsername!.trim()
|
||||
: fallbackUsername.trim(),
|
||||
token: token,
|
||||
refreshToken: effectiveRefreshToken,
|
||||
role: effectiveRole,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extractPayload(Map<String, dynamic> data) {
|
||||
final dynamic nested = data['data'] ?? data['result'] ?? data['payload'];
|
||||
if (nested is Map<String, dynamic>) {
|
||||
return nested;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
String? _pickString(Map<String, dynamic> data, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = data[key];
|
||||
if (value != null && value.toString().trim().isNotEmpty) {
|
||||
return value.toString().trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,31 @@ class MockAuthRepository implements AuthRepository {
|
||||
throw Exception('Password must be at least 6 characters.');
|
||||
}
|
||||
|
||||
return LoginUser(username: username.trim());
|
||||
return LoginUser(
|
||||
username: username.trim(),
|
||||
token: 'mock-access-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
role: username.trim().toLowerCase() == 'admin' ? 'admin' : 'user',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoginUser> refreshSession({
|
||||
required String username,
|
||||
required String refreshToken,
|
||||
required String role,
|
||||
}) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
if (refreshToken.trim().isEmpty) {
|
||||
throw Exception('Refresh token is required.');
|
||||
}
|
||||
|
||||
return LoginUser(
|
||||
username: username.trim(),
|
||||
token: 'mock-access-token-refreshed',
|
||||
refreshToken: 'mock-refresh-token-refreshed',
|
||||
role: role,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,15 @@
|
||||
class LoginUser {
|
||||
const LoginUser({required this.username});
|
||||
const LoginUser({
|
||||
required this.username,
|
||||
required this.token,
|
||||
required this.refreshToken,
|
||||
required this.role,
|
||||
});
|
||||
|
||||
final String username;
|
||||
final String token;
|
||||
final String refreshToken;
|
||||
final String role;
|
||||
|
||||
bool get isAdmin => role.toLowerCase() == 'admin';
|
||||
}
|
||||
|
||||
@ -2,4 +2,10 @@ import 'package:e_receipt_mobile/domain/entities/login_user.dart';
|
||||
|
||||
abstract class AuthRepository {
|
||||
Future<LoginUser> login({required String username, required String password});
|
||||
|
||||
Future<LoginUser> refreshSession({
|
||||
required String username,
|
||||
required String refreshToken,
|
||||
required String role,
|
||||
});
|
||||
}
|
||||
|
||||
19
lib/presentation/auth/session_controller.dart
Normal file
19
lib/presentation/auth/session_controller.dart
Normal file
@ -0,0 +1,19 @@
|
||||
import 'package:e_receipt_mobile/domain/entities/login_user.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final sessionControllerProvider =
|
||||
StateNotifierProvider<SessionController, LoginUser?>((ref) {
|
||||
return SessionController();
|
||||
});
|
||||
|
||||
class SessionController extends StateNotifier<LoginUser?> {
|
||||
SessionController() : super(null);
|
||||
|
||||
void setUser(LoginUser user) {
|
||||
state = user;
|
||||
}
|
||||
|
||||
void clearUser() {
|
||||
state = null;
|
||||
}
|
||||
}
|
||||
@ -5,12 +5,14 @@ Widget roundedInput({
|
||||
required String hint,
|
||||
required IconData icon,
|
||||
bool isPassword = false,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
return RoundedInput(
|
||||
controller: controller,
|
||||
hint: hint,
|
||||
icon: icon,
|
||||
isPassword: isPassword,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
|
||||
@ -21,12 +23,14 @@ class RoundedInput extends StatefulWidget {
|
||||
required this.hint,
|
||||
required this.icon,
|
||||
this.isPassword = false,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String hint;
|
||||
final IconData icon;
|
||||
final bool isPassword;
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
@override
|
||||
State<RoundedInput> createState() => _RoundedInputState();
|
||||
@ -43,9 +47,10 @@ class _RoundedInputState extends State<RoundedInput> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
return TextFormField(
|
||||
controller: widget.controller,
|
||||
obscureText: _obscured,
|
||||
validator: widget.validator,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hint,
|
||||
prefixIcon: Icon(widget.icon, color: Colors.grey),
|
||||
|
||||
@ -1,17 +1,28 @@
|
||||
import 'package:e_receipt_mobile/domain/entities/login_user.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key});
|
||||
const HomeScreen({required this.user, super.key});
|
||||
|
||||
final LoginUser user;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final role = user.role.toLowerCase();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Home'),
|
||||
title: Text(role == 'admin' ? 'Admin Home' : 'User Home'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Welcome to Home Screen'),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Welcome ${user.username}'),
|
||||
const SizedBox(height: 8),
|
||||
Text('Role: ${user.role}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -43,17 +43,18 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
}
|
||||
|
||||
if (next.successMessage != null &&
|
||||
next.user != null &&
|
||||
previous?.successMessage != next.successMessage) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const HomeScreen(),
|
||||
builder: (_) => HomeScreen(user: next.user!),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.blue,
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(32),
|
||||
@ -79,10 +80,34 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
//Username
|
||||
roundedInput(controller: _usernameController, hint: "Username", icon: Icons.person),
|
||||
roundedInput(
|
||||
controller: _usernameController,
|
||||
hint: "Username",
|
||||
icon: Icons.person,
|
||||
validator: (value) {
|
||||
if ((value ?? '').trim().isEmpty) {
|
||||
return 'Username is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
//Password
|
||||
roundedInput(controller: _passwordController, hint: "Password", icon: Icons.lock, isPassword: true),
|
||||
roundedInput(
|
||||
controller: _passwordController,
|
||||
hint: "Password",
|
||||
icon: Icons.lock,
|
||||
isPassword: true,
|
||||
validator: (value) {
|
||||
if ((value ?? '').isEmpty) {
|
||||
return 'Password is required';
|
||||
}
|
||||
if (value!.length < 6) {
|
||||
return 'Password must be at least 6 characters';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton(
|
||||
onPressed: state.isLoading
|
||||
|
||||
@ -1,18 +1,25 @@
|
||||
import 'package:e_receipt_mobile/domain/entities/login_user.dart';
|
||||
|
||||
class LoginState {
|
||||
const LoginState({
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
this.successMessage,
|
||||
this.user,
|
||||
});
|
||||
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
final String? successMessage;
|
||||
final LoginUser? user;
|
||||
|
||||
static const Object _unset = Object();
|
||||
|
||||
LoginState copyWith({
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
String? successMessage,
|
||||
Object? user = _unset,
|
||||
bool clearError = false,
|
||||
bool clearSuccess = false,
|
||||
}) {
|
||||
@ -22,6 +29,7 @@ class LoginState {
|
||||
successMessage: clearSuccess
|
||||
? null
|
||||
: successMessage ?? this.successMessage,
|
||||
user: identical(user, _unset) ? this.user : user as LoginUser?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,50 +1,102 @@
|
||||
import 'package:e_receipt_mobile/core/config/app_config.dart';
|
||||
import 'package:e_receipt_mobile/core/network/auth_http_client.dart';
|
||||
import 'package:e_receipt_mobile/core/network/logging_http_client.dart';
|
||||
import 'package:e_receipt_mobile/data/repositories/api_auth_repository.dart';
|
||||
import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart';
|
||||
import 'package:e_receipt_mobile/presentation/auth/session_controller.dart';
|
||||
import 'package:e_receipt_mobile/presentation/login/login_state.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
final httpClientProvider = Provider<http.Client>((ref) {
|
||||
final rawHttpClientProvider = Provider<http.Client>((ref) {
|
||||
final client = LoggingHttpClient();
|
||||
ref.onDispose(client.close);
|
||||
return client;
|
||||
});
|
||||
|
||||
final authenticatedHttpClientProvider = Provider<http.Client>((ref) {
|
||||
final client = AuthHttpClient(
|
||||
innerClient: LoggingHttpClient(),
|
||||
accessTokenProvider: () => ref.read(sessionControllerProvider)?.token,
|
||||
shouldRefreshOnUnauthorized: (uri) =>
|
||||
!uri.path.endsWith('/receipt/auth/login') &&
|
||||
!uri.path.endsWith('/receipt/auth/refresh-token'),
|
||||
refreshAccessToken: () async {
|
||||
final sessionUser = ref.read(sessionControllerProvider);
|
||||
if (sessionUser == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final refreshedUser = await ref.read(authRepositoryProvider).refreshSession(
|
||||
username: sessionUser.username,
|
||||
refreshToken: sessionUser.refreshToken,
|
||||
role: sessionUser.role,
|
||||
);
|
||||
ref.read(sessionControllerProvider.notifier).setUser(refreshedUser);
|
||||
return refreshedUser.token;
|
||||
} catch (_) {
|
||||
ref.read(sessionControllerProvider.notifier).clearUser();
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
ref.onDispose(client.close);
|
||||
return client;
|
||||
});
|
||||
|
||||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||
return ApiAuthRepository(
|
||||
baseUrl: AppConfig.apiBaseUrl,
|
||||
eReceiptSecret: AppConfig.eReceiptSecret,
|
||||
client: ref.watch(httpClientProvider),
|
||||
apiSecret: AppConfig.apiSecret,
|
||||
client: ref.watch(rawHttpClientProvider),
|
||||
);
|
||||
});
|
||||
|
||||
final loginViewModelProvider =
|
||||
StateNotifierProvider<LoginViewModel, LoginState>((ref) {
|
||||
return LoginViewModel(ref.watch(authRepositoryProvider));
|
||||
return LoginViewModel(
|
||||
ref.watch(authRepositoryProvider),
|
||||
ref.watch(sessionControllerProvider.notifier),
|
||||
);
|
||||
});
|
||||
|
||||
class LoginViewModel extends StateNotifier<LoginState> {
|
||||
LoginViewModel(this._authRepository) : super(const LoginState());
|
||||
LoginViewModel(this._authRepository, this._sessionController)
|
||||
: super(const LoginState());
|
||||
|
||||
final AuthRepository _authRepository;
|
||||
final SessionController _sessionController;
|
||||
|
||||
Future<void> login({required String username, required String password}) async {
|
||||
final trimmedUsername = username.trim();
|
||||
if (trimmedUsername.isEmpty || password.isEmpty) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: 'Username and password are required',
|
||||
clearSuccess: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
isLoading: true,
|
||||
clearError: true,
|
||||
clearSuccess: true,
|
||||
user: null,
|
||||
);
|
||||
_sessionController.clearUser();
|
||||
|
||||
try {
|
||||
final user = await _authRepository.login(
|
||||
username: username,
|
||||
username: trimmedUsername,
|
||||
password: password,
|
||||
);
|
||||
_sessionController.setUser(user);
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
successMessage: 'Welcome ${user.username}',
|
||||
user: user,
|
||||
successMessage: 'Welcome ${user.username} (${user.role})',
|
||||
);
|
||||
} catch (error) {
|
||||
state = state.copyWith(
|
||||
|
||||
@ -41,14 +41,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@ -36,7 +36,6 @@ dependencies:
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_riverpod: ^2.6.1
|
||||
http: ^1.2.2
|
||||
crypto: ^3.0.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user