mirror of https://github.com/flutter/samples.git
Merge branch 'compass-app' of https://github.com/flutter/samples into compass-app
commit
536c8e8ccb
|
After Width: | Height: | Size: 9.9 KiB |
@ -0,0 +1,21 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
|
||||
/// Repository to save and get auth token.
|
||||
/// Notifies listeners when the token changes e.g. user logs out.
|
||||
abstract class AuthTokenRepository extends ChangeNotifier {
|
||||
/// Get the token.
|
||||
/// If the value is null, usually means that the user is logged out.
|
||||
Future<Result<String?>> getToken();
|
||||
|
||||
/// Store the token.
|
||||
/// Will notifiy listeners.
|
||||
Future<Result<void>> saveToken(String? token);
|
||||
|
||||
/// Returns true when the token exists, otherwise false.
|
||||
Future<bool> hasToken() async {
|
||||
final result = await getToken();
|
||||
return result is Ok<String?> && result.value != null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import '../../../utils/result.dart';
|
||||
import 'auth_token_repository.dart';
|
||||
|
||||
/// Development [AuthTokenRepository] that always returns a fake token
|
||||
class AuthTokenRepositoryDev extends AuthTokenRepository {
|
||||
@override
|
||||
Future<Result<String?>> getToken() async {
|
||||
return Result.ok('token');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> saveToken(String? token) async {
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import 'auth_token_repository.dart';
|
||||
|
||||
/// [AuthTokenRepository] that stores the token in Shared Preferences.
|
||||
/// Provided for demo purposes, consider using a secure store instead.
|
||||
class AuthTokenRepositorySharedPrefs extends AuthTokenRepository {
|
||||
static const _tokenKey = 'TOKEN';
|
||||
String? cachedToken;
|
||||
|
||||
@override
|
||||
Future<Result<String?>> getToken() async {
|
||||
if (cachedToken != null) return Result.ok(cachedToken);
|
||||
|
||||
try {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final token = sharedPreferences.getString(_tokenKey);
|
||||
return Result.ok(token);
|
||||
} on Exception catch (e) {
|
||||
return Result.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> saveToken(String? token) async {
|
||||
try {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
if (token == null) {
|
||||
await sharedPreferences.remove(_tokenKey);
|
||||
} else {
|
||||
await sharedPreferences.setString(_tokenKey, token);
|
||||
}
|
||||
cachedToken = token;
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
} on Exception catch (e) {
|
||||
return Result.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import '../../../data/repositories/auth/auth_token_repository.dart';
|
||||
import '../../../data/services/api_client.dart';
|
||||
|
||||
/// Performs user login.
|
||||
class AuthLoginComponent {
|
||||
AuthLoginComponent({
|
||||
required AuthTokenRepository authTokenRepository,
|
||||
required ApiClient apiClient,
|
||||
}) : _authTokenRepository = authTokenRepository,
|
||||
_apiClient = apiClient;
|
||||
|
||||
final AuthTokenRepository _authTokenRepository;
|
||||
final ApiClient _apiClient;
|
||||
final _log = Logger('AuthLoginComponent');
|
||||
|
||||
/// Login with username and password.
|
||||
/// Performs login with the server and stores the obtained auth token.
|
||||
Future<Result<void>> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
final result = await _apiClient.login(
|
||||
LoginRequest(
|
||||
email: email,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
switch (result) {
|
||||
case Ok<LoginResponse>():
|
||||
_log.info('User logged int');
|
||||
return await _authTokenRepository.saveToken(result.value.token);
|
||||
case Error<LoginResponse>():
|
||||
_log.warning('Error logging in: ${result.error}');
|
||||
return Result.error(result.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import '../../../data/repositories/auth/auth_token_repository.dart';
|
||||
import '../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
|
||||
|
||||
/// Performs user logout.
|
||||
class AuthLogoutComponent {
|
||||
AuthLogoutComponent({
|
||||
required AuthTokenRepository authTokenRepository,
|
||||
required ItineraryConfigRepository itineraryConfigRepository,
|
||||
}) : _authTokenRepository = authTokenRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository;
|
||||
|
||||
final AuthTokenRepository _authTokenRepository;
|
||||
final ItineraryConfigRepository _itineraryConfigRepository;
|
||||
final _log = Logger('AuthLogoutComponent');
|
||||
|
||||
/// Perform user logout.
|
||||
///
|
||||
/// 1. Clears the stored ItineraryConfig.
|
||||
/// 2. Clears the stored auth token.
|
||||
///
|
||||
/// GoRouter will automatically redirect the user to /login
|
||||
Future<Result<void>> logout() async {
|
||||
_log.info('User logged out');
|
||||
|
||||
// Clear stored ItineraryConfig
|
||||
var result = await _itineraryConfigRepository
|
||||
.setItineraryConfig(const ItineraryConfig());
|
||||
if (result is Error<void>) {
|
||||
_log.severe('Failed to clear stored ItineraryConfig');
|
||||
return result;
|
||||
}
|
||||
|
||||
// Clear stored auth token
|
||||
result = await _authTokenRepository.saveToken(null);
|
||||
if (result is Error<void>) {
|
||||
_log.severe('Failed to clear stored auth token');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../../domain/components/auth/auth_login_component.dart';
|
||||
import '../../../../utils/command.dart';
|
||||
import '../../../../utils/result.dart';
|
||||
|
||||
class LoginViewModel {
|
||||
LoginViewModel({
|
||||
required AuthLoginComponent authLoginComponent,
|
||||
}) : _authLoginComponent = authLoginComponent {
|
||||
login = Command1<void, (String email, String password)>(_login);
|
||||
}
|
||||
|
||||
final AuthLoginComponent _authLoginComponent;
|
||||
final _log = Logger('LoginViewModel');
|
||||
|
||||
late Command1 login;
|
||||
|
||||
Future<Result<void>> _login((String, String) credentials) async {
|
||||
final (email, password) = credentials;
|
||||
final result = await _authLoginComponent.login(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
if (result is Error<void>) {
|
||||
_log.warning('Login failed! ${result.error}');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/localization/applocalization.dart';
|
||||
import '../../../core/themes/dimens.dart';
|
||||
import '../view_models/login_viewmodel.dart';
|
||||
import 'tilted_cards.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final LoginViewModel viewModel;
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
final TextEditingController _email =
|
||||
TextEditingController(text: 'email@example.com');
|
||||
final TextEditingController _password =
|
||||
TextEditingController(text: 'password');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.viewModel.login.addListener(_onResult);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LoginScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
oldWidget.viewModel.login.removeListener(_onResult);
|
||||
widget.viewModel.login.addListener(_onResult);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.viewModel.login.removeListener(_onResult);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const TiltedCards(),
|
||||
Padding(
|
||||
padding: Dimens.of(context).edgeInsetsScreenSymmetric,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _email,
|
||||
),
|
||||
const SizedBox(height: Dimens.paddingVertical),
|
||||
TextField(
|
||||
controller: _password,
|
||||
obscureText: true,
|
||||
),
|
||||
const SizedBox(height: Dimens.paddingVertical),
|
||||
ListenableBuilder(
|
||||
listenable: widget.viewModel.login,
|
||||
builder: (context, _) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
widget.viewModel.login
|
||||
.execute((_email.value.text, _password.value.text));
|
||||
},
|
||||
child: Text(AppLocalization.of(context).login),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onResult() {
|
||||
if (widget.viewModel.login.completed) {
|
||||
widget.viewModel.login.clearResult();
|
||||
context.go('/');
|
||||
}
|
||||
|
||||
if (widget.viewModel.login.error) {
|
||||
widget.viewModel.login.clearResult();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalization.of(context).errorWhileLogin),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: () => widget.viewModel.login
|
||||
.execute((_email.value.text, _password.value.text)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class TiltedCards extends StatelessWidget {
|
||||
const TiltedCards({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: _Card(
|
||||
imageUrl: 'https://rstr.in/google/tripedia/g2i0BsYPKW-',
|
||||
width: 200,
|
||||
height: 273,
|
||||
tilt: -3.83 / 360,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: _Card(
|
||||
imageUrl: 'https://rstr.in/google/tripedia/980sqNgaDRK',
|
||||
width: 180,
|
||||
height: 230,
|
||||
tilt: 3.46 / 360,
|
||||
),
|
||||
),
|
||||
_Card(
|
||||
imageUrl: 'https://rstr.in/google/tripedia/pHfPmf3o5NU',
|
||||
width: 225,
|
||||
height: 322,
|
||||
tilt: 0,
|
||||
showTitle: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Card extends StatelessWidget {
|
||||
const _Card({
|
||||
required this.imageUrl,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.tilt,
|
||||
this.showTitle = false,
|
||||
});
|
||||
|
||||
final double tilt;
|
||||
final double width;
|
||||
final double height;
|
||||
final String imageUrl;
|
||||
final bool showTitle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RotationTransition(
|
||||
turns: AlwaysStoppedAnimation(tilt),
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
color: showTitle ? Colors.black.withOpacity(0.5) : null,
|
||||
colorBlendMode: showTitle ? BlendMode.darken : null,
|
||||
),
|
||||
if (showTitle) Center(child: SvgPicture.asset('assets/logo.svg')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import '../../../../domain/components/auth/auth_logout_component.dart';
|
||||
import '../../../../utils/command.dart';
|
||||
import '../../../../utils/result.dart';
|
||||
|
||||
class LogoutViewModel {
|
||||
LogoutViewModel({
|
||||
required AuthLogoutComponent authLogoutComponent,
|
||||
}) : _authLogoutComponent = authLogoutComponent {
|
||||
logout = Command0(_logout);
|
||||
}
|
||||
final AuthLogoutComponent _authLogoutComponent;
|
||||
late Command0 logout;
|
||||
|
||||
Future<Result> _logout() => _authLogoutComponent.logout();
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/localization/applocalization.dart';
|
||||
import '../../../core/themes/colors.dart';
|
||||
import '../view_models/logout_viewmodel.dart';
|
||||
|
||||
class LogoutButton extends StatefulWidget {
|
||||
const LogoutButton({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final LogoutViewModel viewModel;
|
||||
|
||||
@override
|
||||
State<LogoutButton> createState() => _LogoutButtonState();
|
||||
}
|
||||
|
||||
class _LogoutButtonState extends State<LogoutButton> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.viewModel.logout.addListener(_onResult);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LogoutButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
oldWidget.viewModel.logout.removeListener(_onResult);
|
||||
widget.viewModel.logout.addListener(_onResult);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.viewModel.logout.removeListener(_onResult);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 40.0,
|
||||
width: 40.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.grey1),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: InkResponse(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
onTap: () {
|
||||
widget.viewModel.logout.execute();
|
||||
},
|
||||
child: Center(
|
||||
child: Icon(
|
||||
size: 24.0,
|
||||
Icons.logout,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onResult() {
|
||||
// We do not need to navigate to `/login` on logout,
|
||||
// it is done automatically by GoRouter.
|
||||
|
||||
if (widget.viewModel.logout.error) {
|
||||
widget.viewModel.logout.clearResult();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalization.of(context).errorWhileLogout),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: widget.viewModel.logout.execute,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import 'package:compass_app/domain/components/auth/auth_login_component.dart';
|
||||
import 'package:compass_app/data/services/api_client.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../../testing/fakes/repositories/fake_auth_token_repository.dart';
|
||||
import '../../../../testing/fakes/services/fake_api_client.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthLoginComponent test', () {
|
||||
late AuthLoginComponent authLoginComponent;
|
||||
late FakeAuthTokenRepository authTokenRepository;
|
||||
late ApiClient apiClient;
|
||||
|
||||
setUp(() {
|
||||
authTokenRepository = FakeAuthTokenRepository();
|
||||
apiClient = _ApiClient();
|
||||
authLoginComponent = AuthLoginComponent(
|
||||
authTokenRepository: authTokenRepository,
|
||||
apiClient: apiClient,
|
||||
);
|
||||
});
|
||||
|
||||
test('should perform login', () async {
|
||||
// Pass valid email and password
|
||||
final result = await authLoginComponent.login(
|
||||
email: 'EMAIL',
|
||||
password: 'PASSWORD',
|
||||
);
|
||||
// Got good response
|
||||
expect(result, isA<Ok<void>>());
|
||||
expect(authTokenRepository.token, 'TOKEN');
|
||||
});
|
||||
|
||||
test('should fail to login', () async {
|
||||
// Pass wrong email and password
|
||||
final result = await authLoginComponent.login(
|
||||
email: 'WRONG',
|
||||
password: 'WRONG',
|
||||
);
|
||||
// Got bad response
|
||||
expect(result, isA<Error<void>>());
|
||||
expect(result.asError.error.toString(), 'Exception: ERROR');
|
||||
expect(authTokenRepository.token, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _ApiClient extends FakeApiClient {
|
||||
@override
|
||||
Future<Result<LoginResponse>> login(LoginRequest loginRequest) async {
|
||||
if (loginRequest.email == 'EMAIL' && loginRequest.password == 'PASSWORD') {
|
||||
return Result.ok(const LoginResponse(token: 'TOKEN', userId: '1234'));
|
||||
} else {
|
||||
return Result.error(Exception('ERROR'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import 'package:compass_app/domain/components/auth/auth_logout_component.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../../testing/fakes/repositories/fake_auth_token_repository.dart';
|
||||
import '../../../../testing/fakes/repositories/fake_itinerary_config_repository.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthLogoutComponent test', () {
|
||||
late AuthLogoutComponent authLogoutComponent;
|
||||
late FakeAuthTokenRepository authTokenRepository;
|
||||
late FakeItineraryConfigRepository itineraryConfigRepository;
|
||||
|
||||
setUp(() {
|
||||
authTokenRepository = FakeAuthTokenRepository();
|
||||
itineraryConfigRepository = FakeItineraryConfigRepository(
|
||||
itineraryConfig: const ItineraryConfig(continent: 'CONTINENT'),
|
||||
);
|
||||
authLogoutComponent = AuthLogoutComponent(
|
||||
authTokenRepository: authTokenRepository,
|
||||
itineraryConfigRepository: itineraryConfigRepository,
|
||||
);
|
||||
});
|
||||
|
||||
test('should perform logout', () async {
|
||||
await authLogoutComponent.logout();
|
||||
// Token should be removed
|
||||
expect(authTokenRepository.token, null);
|
||||
// Itinerary config should be cleared
|
||||
expect(
|
||||
itineraryConfigRepository.itineraryConfig,
|
||||
const ItineraryConfig(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
import 'package:compass_app/ui/booking/components/booking_create_component.dart';
|
||||
import 'package:compass_app/domain/components/booking/booking_create_component.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../testing/fakes/repositories/fake_activities_repository.dart';
|
||||
import '../../../testing/fakes/repositories/fake_destination_repository.dart';
|
||||
import '../../../testing/models/activity.dart';
|
||||
import '../../../testing/models/booking.dart';
|
||||
import '../../../testing/models/destination.dart';
|
||||
import '../../../../testing/fakes/repositories/fake_activities_repository.dart';
|
||||
import '../../../../testing/fakes/repositories/fake_destination_repository.dart';
|
||||
import '../../../../testing/models/activity.dart';
|
||||
import '../../../../testing/models/booking.dart';
|
||||
import '../../../../testing/models/destination.dart';
|
||||
|
||||
void main() {
|
||||
group('BookingCreateComponent tests', () {
|
||||
@ -1,9 +1,9 @@
|
||||
import 'package:compass_app/ui/booking/components/booking_share_component.dart';
|
||||
import 'package:compass_app/domain/components/booking/booking_share_component.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../testing/models/activity.dart';
|
||||
import '../../../testing/models/destination.dart';
|
||||
import '../../../../testing/models/activity.dart';
|
||||
import '../../../../testing/models/destination.dart';
|
||||
|
||||
void main() {
|
||||
group('BookingShareComponent tests', () {
|
||||
@ -0,0 +1,64 @@
|
||||
import 'package:compass_app/domain/components/auth/auth_login_component.dart';
|
||||
import 'package:compass_app/ui/auth/login/view_models/login_viewmodel.dart';
|
||||
import 'package:compass_app/ui/auth/login/widgets/login_screen.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:mocktail_image_network/mocktail_image_network.dart';
|
||||
|
||||
import '../../../testing/app.dart';
|
||||
import '../../../testing/fakes/repositories/fake_auth_token_repository.dart';
|
||||
import '../../../testing/fakes/services/fake_api_client.dart';
|
||||
import '../../../testing/mocks.dart';
|
||||
|
||||
void main() {
|
||||
group('LoginScreen test', () {
|
||||
late LoginViewModel viewModel;
|
||||
late MockGoRouter goRouter;
|
||||
late FakeAuthTokenRepository fakeAuthTokenRepository;
|
||||
|
||||
setUp(() {
|
||||
fakeAuthTokenRepository = FakeAuthTokenRepository();
|
||||
viewModel = LoginViewModel(
|
||||
authLoginComponent: AuthLoginComponent(
|
||||
authTokenRepository: fakeAuthTokenRepository,
|
||||
apiClient: FakeApiClient(),
|
||||
),
|
||||
);
|
||||
goRouter = MockGoRouter();
|
||||
});
|
||||
|
||||
Future<void> loadScreen(WidgetTester tester) async {
|
||||
await testApp(
|
||||
tester,
|
||||
LoginScreen(viewModel: viewModel),
|
||||
goRouter: goRouter,
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('should load screen', (WidgetTester tester) async {
|
||||
await mockNetworkImages(() async {
|
||||
await loadScreen(tester);
|
||||
expect(find.byType(LoginScreen), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('should perform login', (WidgetTester tester) async {
|
||||
await mockNetworkImages(() async {
|
||||
await loadScreen(tester);
|
||||
|
||||
// Repo should have no key
|
||||
expect(fakeAuthTokenRepository.token, null);
|
||||
|
||||
// Perform login
|
||||
await tester.tap(find.text('Login'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Repo should have key
|
||||
expect(fakeAuthTokenRepository.token, 'TOKEN');
|
||||
|
||||
// Should navigate to home screen
|
||||
verify(() => goRouter.go('/')).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import 'package:compass_app/domain/components/auth/auth_logout_component.dart';
|
||||
import 'package:compass_app/ui/auth/logout/view_models/logout_viewmodel.dart';
|
||||
import 'package:compass_app/ui/auth/logout/widgets/logout_button.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail_image_network/mocktail_image_network.dart';
|
||||
|
||||
import '../../../testing/app.dart';
|
||||
import '../../../testing/fakes/repositories/fake_auth_token_repository.dart';
|
||||
import '../../../testing/fakes/repositories/fake_itinerary_config_repository.dart';
|
||||
import '../../../testing/mocks.dart';
|
||||
|
||||
void main() {
|
||||
group('LogoutButton test', () {
|
||||
late MockGoRouter goRouter;
|
||||
late FakeAuthTokenRepository fakeAuthTokenRepository;
|
||||
late FakeItineraryConfigRepository fakeItineraryConfigRepository;
|
||||
late LogoutViewModel viewModel;
|
||||
|
||||
setUp(() {
|
||||
goRouter = MockGoRouter();
|
||||
fakeAuthTokenRepository = FakeAuthTokenRepository();
|
||||
// Setup a token, should be cleared after logout
|
||||
fakeAuthTokenRepository.token = 'TOKEN';
|
||||
// Setup an ItineraryConfig with some data, should be cleared after logout
|
||||
fakeItineraryConfigRepository = FakeItineraryConfigRepository(
|
||||
itineraryConfig: const ItineraryConfig(continent: 'CONTINENT'));
|
||||
viewModel = LogoutViewModel(
|
||||
authLogoutComponent: AuthLogoutComponent(
|
||||
authTokenRepository: fakeAuthTokenRepository,
|
||||
itineraryConfigRepository: fakeItineraryConfigRepository,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
Future<void> loadScreen(WidgetTester tester) async {
|
||||
await testApp(
|
||||
tester,
|
||||
LogoutButton(viewModel: viewModel),
|
||||
goRouter: goRouter,
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('should load widget', (WidgetTester tester) async {
|
||||
await mockNetworkImages(() async {
|
||||
await loadScreen(tester);
|
||||
expect(find.byType(LogoutButton), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('should perform logout', (WidgetTester tester) async {
|
||||
await mockNetworkImages(() async {
|
||||
await loadScreen(tester);
|
||||
|
||||
// Repo should have a key
|
||||
expect(fakeAuthTokenRepository.token, 'TOKEN');
|
||||
// Itinerary config should have data
|
||||
expect(
|
||||
fakeItineraryConfigRepository.itineraryConfig,
|
||||
const ItineraryConfig(continent: 'CONTINENT'),
|
||||
);
|
||||
|
||||
// // Perform logout
|
||||
await tester.tap(find.byType(LogoutButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Repo should have no key
|
||||
expect(fakeAuthTokenRepository.token, null);
|
||||
// Itinerary config should be cleared
|
||||
expect(
|
||||
fakeItineraryConfigRepository.itineraryConfig,
|
||||
const ItineraryConfig(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import 'package:compass_app/data/repositories/auth/auth_token_repository.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
|
||||
class FakeAuthTokenRepository extends AuthTokenRepository {
|
||||
String? token;
|
||||
|
||||
@override
|
||||
Future<Result<String?>> getToken() async {
|
||||
return Result.ok(token);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> saveToken(String? token) async {
|
||||
this.token = token;
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'login_request.freezed.dart';
|
||||
|
||||
part 'login_request.g.dart';
|
||||
|
||||
/// Simple data class to hold login request data.
|
||||
@freezed
|
||||
class LoginRequest with _$LoginRequest {
|
||||
const factory LoginRequest({
|
||||
/// Email address.
|
||||
required String email,
|
||||
|
||||
/// Plain text password.
|
||||
required String password,
|
||||
}) = _LoginRequest;
|
||||
|
||||
factory LoginRequest.fromJson(Map<String, Object?> json) =>
|
||||
_$LoginRequestFromJson(json);
|
||||
}
|
||||
@ -0,0 +1,192 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'login_request.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
LoginRequest _$LoginRequestFromJson(Map<String, dynamic> json) {
|
||||
return _LoginRequest.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LoginRequest {
|
||||
/// Email address.
|
||||
String get email => throw _privateConstructorUsedError;
|
||||
|
||||
/// Plain text password.
|
||||
String get password => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this LoginRequest to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of LoginRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$LoginRequestCopyWith<LoginRequest> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $LoginRequestCopyWith<$Res> {
|
||||
factory $LoginRequestCopyWith(
|
||||
LoginRequest value, $Res Function(LoginRequest) then) =
|
||||
_$LoginRequestCopyWithImpl<$Res, LoginRequest>;
|
||||
@useResult
|
||||
$Res call({String email, String password});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LoginRequestCopyWithImpl<$Res, $Val extends LoginRequest>
|
||||
implements $LoginRequestCopyWith<$Res> {
|
||||
_$LoginRequestCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of LoginRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? email = null,
|
||||
Object? password = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
email: null == email
|
||||
? _value.email
|
||||
: email // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$LoginRequestImplCopyWith<$Res>
|
||||
implements $LoginRequestCopyWith<$Res> {
|
||||
factory _$$LoginRequestImplCopyWith(
|
||||
_$LoginRequestImpl value, $Res Function(_$LoginRequestImpl) then) =
|
||||
__$$LoginRequestImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String email, String password});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$LoginRequestImplCopyWithImpl<$Res>
|
||||
extends _$LoginRequestCopyWithImpl<$Res, _$LoginRequestImpl>
|
||||
implements _$$LoginRequestImplCopyWith<$Res> {
|
||||
__$$LoginRequestImplCopyWithImpl(
|
||||
_$LoginRequestImpl _value, $Res Function(_$LoginRequestImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of LoginRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? email = null,
|
||||
Object? password = null,
|
||||
}) {
|
||||
return _then(_$LoginRequestImpl(
|
||||
email: null == email
|
||||
? _value.email
|
||||
: email // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$LoginRequestImpl implements _LoginRequest {
|
||||
const _$LoginRequestImpl({required this.email, required this.password});
|
||||
|
||||
factory _$LoginRequestImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$LoginRequestImplFromJson(json);
|
||||
|
||||
/// Email address.
|
||||
@override
|
||||
final String email;
|
||||
|
||||
/// Plain text password.
|
||||
@override
|
||||
final String password;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoginRequest(email: $email, password: $password)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$LoginRequestImpl &&
|
||||
(identical(other.email, email) || other.email == email) &&
|
||||
(identical(other.password, password) ||
|
||||
other.password == password));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, email, password);
|
||||
|
||||
/// Create a copy of LoginRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$LoginRequestImplCopyWith<_$LoginRequestImpl> get copyWith =>
|
||||
__$$LoginRequestImplCopyWithImpl<_$LoginRequestImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$LoginRequestImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _LoginRequest implements LoginRequest {
|
||||
const factory _LoginRequest(
|
||||
{required final String email,
|
||||
required final String password}) = _$LoginRequestImpl;
|
||||
|
||||
factory _LoginRequest.fromJson(Map<String, dynamic> json) =
|
||||
_$LoginRequestImpl.fromJson;
|
||||
|
||||
/// Email address.
|
||||
@override
|
||||
String get email;
|
||||
|
||||
/// Plain text password.
|
||||
@override
|
||||
String get password;
|
||||
|
||||
/// Create a copy of LoginRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$LoginRequestImplCopyWith<_$LoginRequestImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'login_request.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$LoginRequestImpl _$$LoginRequestImplFromJson(Map<String, dynamic> json) =>
|
||||
_$LoginRequestImpl(
|
||||
email: json['email'] as String,
|
||||
password: json['password'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$LoginRequestImplToJson(_$LoginRequestImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'email': instance.email,
|
||||
'password': instance.password,
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'login_response.freezed.dart';
|
||||
|
||||
part 'login_response.g.dart';
|
||||
|
||||
/// LoginResponse model.
|
||||
@freezed
|
||||
class LoginResponse with _$LoginResponse {
|
||||
const factory LoginResponse({
|
||||
/// The token to be used for authentication.
|
||||
required String token,
|
||||
|
||||
/// The user id.
|
||||
required String userId,
|
||||
}) = _LoginResponse;
|
||||
|
||||
factory LoginResponse.fromJson(Map<String, Object?> json) =>
|
||||
_$LoginResponseFromJson(json);
|
||||
}
|
||||
@ -0,0 +1,191 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'login_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
LoginResponse _$LoginResponseFromJson(Map<String, dynamic> json) {
|
||||
return _LoginResponse.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LoginResponse {
|
||||
/// The token to be used for authentication.
|
||||
String get token => throw _privateConstructorUsedError;
|
||||
|
||||
/// The user id.
|
||||
String get userId => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this LoginResponse to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of LoginResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$LoginResponseCopyWith<LoginResponse> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $LoginResponseCopyWith<$Res> {
|
||||
factory $LoginResponseCopyWith(
|
||||
LoginResponse value, $Res Function(LoginResponse) then) =
|
||||
_$LoginResponseCopyWithImpl<$Res, LoginResponse>;
|
||||
@useResult
|
||||
$Res call({String token, String userId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LoginResponseCopyWithImpl<$Res, $Val extends LoginResponse>
|
||||
implements $LoginResponseCopyWith<$Res> {
|
||||
_$LoginResponseCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of LoginResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? token = null,
|
||||
Object? userId = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
token: null == token
|
||||
? _value.token
|
||||
: token // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
userId: null == userId
|
||||
? _value.userId
|
||||
: userId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$LoginResponseImplCopyWith<$Res>
|
||||
implements $LoginResponseCopyWith<$Res> {
|
||||
factory _$$LoginResponseImplCopyWith(
|
||||
_$LoginResponseImpl value, $Res Function(_$LoginResponseImpl) then) =
|
||||
__$$LoginResponseImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String token, String userId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$LoginResponseImplCopyWithImpl<$Res>
|
||||
extends _$LoginResponseCopyWithImpl<$Res, _$LoginResponseImpl>
|
||||
implements _$$LoginResponseImplCopyWith<$Res> {
|
||||
__$$LoginResponseImplCopyWithImpl(
|
||||
_$LoginResponseImpl _value, $Res Function(_$LoginResponseImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of LoginResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? token = null,
|
||||
Object? userId = null,
|
||||
}) {
|
||||
return _then(_$LoginResponseImpl(
|
||||
token: null == token
|
||||
? _value.token
|
||||
: token // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
userId: null == userId
|
||||
? _value.userId
|
||||
: userId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$LoginResponseImpl implements _LoginResponse {
|
||||
const _$LoginResponseImpl({required this.token, required this.userId});
|
||||
|
||||
factory _$LoginResponseImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$LoginResponseImplFromJson(json);
|
||||
|
||||
/// The token to be used for authentication.
|
||||
@override
|
||||
final String token;
|
||||
|
||||
/// The user id.
|
||||
@override
|
||||
final String userId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoginResponse(token: $token, userId: $userId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$LoginResponseImpl &&
|
||||
(identical(other.token, token) || other.token == token) &&
|
||||
(identical(other.userId, userId) || other.userId == userId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, token, userId);
|
||||
|
||||
/// Create a copy of LoginResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$LoginResponseImplCopyWith<_$LoginResponseImpl> get copyWith =>
|
||||
__$$LoginResponseImplCopyWithImpl<_$LoginResponseImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$LoginResponseImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _LoginResponse implements LoginResponse {
|
||||
const factory _LoginResponse(
|
||||
{required final String token,
|
||||
required final String userId}) = _$LoginResponseImpl;
|
||||
|
||||
factory _LoginResponse.fromJson(Map<String, dynamic> json) =
|
||||
_$LoginResponseImpl.fromJson;
|
||||
|
||||
/// The token to be used for authentication.
|
||||
@override
|
||||
String get token;
|
||||
|
||||
/// The user id.
|
||||
@override
|
||||
String get userId;
|
||||
|
||||
/// Create a copy of LoginResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$LoginResponseImplCopyWith<_$LoginResponseImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'login_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$LoginResponseImpl _$$LoginResponseImplFromJson(Map<String, dynamic> json) =>
|
||||
_$LoginResponseImpl(
|
||||
token: json['token'] as String,
|
||||
userId: json['userId'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$LoginResponseImplToJson(_$LoginResponseImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'token': instance.token,
|
||||
'userId': instance.userId,
|
||||
};
|
||||
@ -0,0 +1,14 @@
|
||||
class Constants {
|
||||
/// Email for the hardcoded login.
|
||||
static const email = 'email@example.com';
|
||||
|
||||
/// Password for the hardcoded login.
|
||||
static const password = 'password';
|
||||
|
||||
/// Token to be returned on successful login.
|
||||
static const token =
|
||||
' e1c37dfd973353b78bb71df050e2c6e72d53034e148920383968ae49b96f1fd2';
|
||||
|
||||
/// User id to be returned on successful login.
|
||||
static const userId = '123';
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import 'package:shelf/shelf.dart';
|
||||
|
||||
import '../config/constants.dart';
|
||||
|
||||
/// Implements a simple auth Middleware.
|
||||
///
|
||||
/// This is provided as an example for Flutter architectural purposes only
|
||||
/// and shouldn't be used as example on how to implement authentication
|
||||
/// in production.
|
||||
///
|
||||
/// This Middleware checks if the token is present in the request headers,
|
||||
/// otherwise returns a 401 Unauthorized response.
|
||||
///
|
||||
/// This token does not expire and is not secure.
|
||||
Middleware authRequests() => (innerHandler) {
|
||||
return (Request request) async {
|
||||
if (request.url.path != 'login' &&
|
||||
request.headers['Authorization'] != 'Bearer ${Constants.token}') {
|
||||
// If the request is not a login request and the token is not present,
|
||||
// return a 401 Unauthorized response.
|
||||
return Response.unauthorized('Unauthorized');
|
||||
}
|
||||
|
||||
return innerHandler(request);
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:compass_server/config/constants.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
/// Implements a simple login API.
|
||||
///
|
||||
/// This is provided as an example for Flutter architectural purposes only
|
||||
/// and shouldn't be used as example on how to implement authentication
|
||||
/// in production.
|
||||
///
|
||||
/// This API only accepts a fixed email and password for demonstration purposes,
|
||||
/// then returns a hardcoded token and a user id.
|
||||
///
|
||||
/// This token does not expire and is not secure.
|
||||
class LoginApi {
|
||||
Router get router {
|
||||
final router = Router();
|
||||
|
||||
router.post('/', (Request request) async {
|
||||
final body = await request.readAsString();
|
||||
final loginRequest = LoginRequest.fromJson(json.decode(body));
|
||||
|
||||
if (loginRequest.email == Constants.email &&
|
||||
loginRequest.password == Constants.password) {
|
||||
return Response.ok(
|
||||
json.encode(
|
||||
LoginResponse(
|
||||
token: Constants.token,
|
||||
userId: Constants.userId,
|
||||
),
|
||||
),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
}
|
||||
|
||||
return Response.unauthorized('Invalid credentials');
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue