diff --git a/compass_app/app/assets/user.jpg b/compass_app/app/assets/user.jpg new file mode 100644 index 00000000000..1e79068e14c Binary files /dev/null and b/compass_app/app/assets/user.jpg differ diff --git a/compass_app/app/integration_test/app_local_data_test.dart b/compass_app/app/integration_test/app_local_data_test.dart index 7281fce55cb..e631046d68b 100644 --- a/compass_app/app/integration_test/app_local_data_test.dart +++ b/compass_app/app/integration_test/app_local_data_test.dart @@ -44,6 +44,9 @@ void main() { expect(find.byType(HomeScreen), findsOneWidget); await tester.pumpAndSettle(); + // Should show user name + expect(find.text('Sofie\'s Trips'), findsOneWidget); + // Tap on booking (Alaska is created by default) await tester.tap(find.text('Alaska, North America')); await tester.pumpAndSettle(); diff --git a/compass_app/app/integration_test/app_server_data_test.dart b/compass_app/app/integration_test/app_server_data_test.dart index 57ef591391e..88df0f8e99e 100644 --- a/compass_app/app/integration_test/app_server_data_test.dart +++ b/compass_app/app/integration_test/app_server_data_test.dart @@ -84,6 +84,9 @@ void main() { expect(find.byType(HomeScreen), findsOneWidget); await tester.pumpAndSettle(); + // Should show user name + expect(find.text('Sofie\'s Trips'), findsOneWidget); + // Tap on booking (Alaska is created by default) await tester.tap(find.text('Alaska, North America')); await tester.pumpAndSettle(); diff --git a/compass_app/app/lib/config/dependencies.dart b/compass_app/app/lib/config/dependencies.dart index f56975707be..5f633be36c2 100644 --- a/compass_app/app/lib/config/dependencies.dart +++ b/compass_app/app/lib/config/dependencies.dart @@ -7,6 +7,9 @@ import '../data/repositories/auth/auth_repository_remote.dart'; import '../data/repositories/booking/booking_repository.dart'; import '../data/repositories/booking/booking_repository_local.dart'; import '../data/repositories/booking/booking_repository_remote.dart'; +import '../data/repositories/user/user_repository.dart'; +import '../data/repositories/user/user_repository_local.dart'; +import '../data/repositories/user/user_repository_remote.dart'; import '../data/services/api/auth_api_client.dart'; import '../data/services/local/local_data_service.dart'; import '../data/services/shared_preferences_service.dart'; @@ -84,6 +87,11 @@ List get providersRemote { apiClient: context.read(), ) as BookingRepository, ), + Provider( + create: (context) => UserRepositoryRemote( + apiClient: context.read(), + ) as UserRepository, + ), ..._sharedProviders, ]; } @@ -122,6 +130,11 @@ List get providersLocal { Provider.value( value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository, ), + Provider( + create: (context) => UserRepositoryLocal( + localDataService: context.read(), + ) as UserRepository, + ), ..._sharedProviders, ]; } diff --git a/compass_app/app/lib/data/repositories/user/user_repository.dart b/compass_app/app/lib/data/repositories/user/user_repository.dart new file mode 100644 index 00000000000..f1e503bfdc2 --- /dev/null +++ b/compass_app/app/lib/data/repositories/user/user_repository.dart @@ -0,0 +1,8 @@ +import '../../../domain/models/user/user.dart'; +import '../../../utils/result.dart'; + +/// Data source for user related data +abstract class UserRepository { + /// Get current user + Future> getUser(); +} diff --git a/compass_app/app/lib/data/repositories/user/user_repository_local.dart b/compass_app/app/lib/data/repositories/user/user_repository_local.dart new file mode 100644 index 00000000000..d43bece6d97 --- /dev/null +++ b/compass_app/app/lib/data/repositories/user/user_repository_local.dart @@ -0,0 +1,17 @@ +import '../../../domain/models/user/user.dart'; +import '../../../utils/result.dart'; +import '../../services/local/local_data_service.dart'; +import 'user_repository.dart'; + +class UserRepositoryLocal implements UserRepository { + UserRepositoryLocal({ + required LocalDataService localDataService, + }) : _localDataService = localDataService; + + final LocalDataService _localDataService; + + @override + Future> getUser() async { + return Result.ok(_localDataService.getUser()); + } +} diff --git a/compass_app/app/lib/data/repositories/user/user_repository_remote.dart b/compass_app/app/lib/data/repositories/user/user_repository_remote.dart new file mode 100644 index 00000000000..2b2d4b0a527 --- /dev/null +++ b/compass_app/app/lib/data/repositories/user/user_repository_remote.dart @@ -0,0 +1,35 @@ +import '../../../domain/models/user/user.dart'; +import '../../../utils/result.dart'; +import '../../services/api/api_client.dart'; +import '../../services/api/model/user/user_api_model.dart'; +import 'user_repository.dart'; + +class UserRepositoryRemote implements UserRepository { + UserRepositoryRemote({ + required ApiClient apiClient, + }) : _apiClient = apiClient; + + final ApiClient _apiClient; + + User? _cachedData; + + @override + Future> getUser() async { + if (_cachedData != null) { + return Future.value(Result.ok(_cachedData!)); + } + + final result = await _apiClient.getUser(); + switch (result) { + case Ok(): + final user = User( + name: result.value.name, + picture: result.value.picture, + ); + _cachedData = user; + return Result.ok(user); + case Error(): + return Result.error(result.error); + } + } +} diff --git a/compass_app/app/lib/data/services/api/api_client.dart b/compass_app/app/lib/data/services/api/api_client.dart index ad98d00abc6..fb2a3454860 100644 --- a/compass_app/app/lib/data/services/api/api_client.dart +++ b/compass_app/app/lib/data/services/api/api_client.dart @@ -6,6 +6,7 @@ import '../../../domain/models/continent/continent.dart'; import '../../../domain/models/destination/destination.dart'; import '../../../utils/result.dart'; import 'model/booking/booking_api_model.dart'; +import 'model/user/user_api_model.dart'; /// Adds the `Authentication` header to a header configuration. typedef AuthHeaderProvider = String? Function(); @@ -154,4 +155,24 @@ class ApiClient { client.close(); } } + + Future> getUser() async { + final client = HttpClient(); + try { + final request = await client.get('localhost', 8080, '/user'); + await _authHeader(request.headers); + final response = await request.close(); + if (response.statusCode == 200) { + final stringData = await response.transform(utf8.decoder).join(); + final user = UserApiModel.fromJson(jsonDecode(stringData)); + return Result.ok(user); + } else { + return Result.error(const HttpException("Invalid response")); + } + } on Exception catch (error) { + return Result.error(error); + } finally { + client.close(); + } + } } diff --git a/compass_app/app/lib/data/services/api/model/user/user_api_model.dart b/compass_app/app/lib/data/services/api/model/user/user_api_model.dart new file mode 100644 index 00000000000..9806fbb6a30 --- /dev/null +++ b/compass_app/app/lib/data/services/api/model/user/user_api_model.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user_api_model.freezed.dart'; +part 'user_api_model.g.dart'; + +@freezed +abstract class UserApiModel with _$UserApiModel { + const factory UserApiModel({ + /// The user's ID. + required String id, + + /// The user's name. + required String name, + + /// The user's email. + required String email, + + /// The user's picture URL. + required String picture, + }) = _UserApiModel; + + factory UserApiModel.fromJson(Map json) => + _$UserApiModelFromJson(json); +} diff --git a/compass_app/app/lib/data/services/api/model/user/user_api_model.freezed.dart b/compass_app/app/lib/data/services/api/model/user/user_api_model.freezed.dart new file mode 100644 index 00000000000..6725b7f029c --- /dev/null +++ b/compass_app/app/lib/data/services/api/model/user/user_api_model.freezed.dart @@ -0,0 +1,241 @@ +// 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 'user_api_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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'); + +UserApiModel _$UserApiModelFromJson(Map json) { + return _UserApiModel.fromJson(json); +} + +/// @nodoc +mixin _$UserApiModel { + /// The user's ID. + String get id => throw _privateConstructorUsedError; + + /// The user's name. + String get name => throw _privateConstructorUsedError; + + /// The user's email. + String get email => throw _privateConstructorUsedError; + + /// The user's picture URL. + String get picture => throw _privateConstructorUsedError; + + /// Serializes this UserApiModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UserApiModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserApiModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserApiModelCopyWith<$Res> { + factory $UserApiModelCopyWith( + UserApiModel value, $Res Function(UserApiModel) then) = + _$UserApiModelCopyWithImpl<$Res, UserApiModel>; + @useResult + $Res call({String id, String name, String email, String picture}); +} + +/// @nodoc +class _$UserApiModelCopyWithImpl<$Res, $Val extends UserApiModel> + implements $UserApiModelCopyWith<$Res> { + _$UserApiModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserApiModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? email = null, + Object? picture = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + picture: null == picture + ? _value.picture + : picture // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserApiModelImplCopyWith<$Res> + implements $UserApiModelCopyWith<$Res> { + factory _$$UserApiModelImplCopyWith( + _$UserApiModelImpl value, $Res Function(_$UserApiModelImpl) then) = + __$$UserApiModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String id, String name, String email, String picture}); +} + +/// @nodoc +class __$$UserApiModelImplCopyWithImpl<$Res> + extends _$UserApiModelCopyWithImpl<$Res, _$UserApiModelImpl> + implements _$$UserApiModelImplCopyWith<$Res> { + __$$UserApiModelImplCopyWithImpl( + _$UserApiModelImpl _value, $Res Function(_$UserApiModelImpl) _then) + : super(_value, _then); + + /// Create a copy of UserApiModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? email = null, + Object? picture = null, + }) { + return _then(_$UserApiModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + picture: null == picture + ? _value.picture + : picture // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserApiModelImpl implements _UserApiModel { + const _$UserApiModelImpl( + {required this.id, + required this.name, + required this.email, + required this.picture}); + + factory _$UserApiModelImpl.fromJson(Map json) => + _$$UserApiModelImplFromJson(json); + + /// The user's ID. + @override + final String id; + + /// The user's name. + @override + final String name; + + /// The user's email. + @override + final String email; + + /// The user's picture URL. + @override + final String picture; + + @override + String toString() { + return 'UserApiModel(id: $id, name: $name, email: $email, picture: $picture)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserApiModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.picture, picture) || other.picture == picture)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, name, email, picture); + + /// Create a copy of UserApiModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserApiModelImplCopyWith<_$UserApiModelImpl> get copyWith => + __$$UserApiModelImplCopyWithImpl<_$UserApiModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$UserApiModelImplToJson( + this, + ); + } +} + +abstract class _UserApiModel implements UserApiModel { + const factory _UserApiModel( + {required final String id, + required final String name, + required final String email, + required final String picture}) = _$UserApiModelImpl; + + factory _UserApiModel.fromJson(Map json) = + _$UserApiModelImpl.fromJson; + + /// The user's ID. + @override + String get id; + + /// The user's name. + @override + String get name; + + /// The user's email. + @override + String get email; + + /// The user's picture URL. + @override + String get picture; + + /// Create a copy of UserApiModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserApiModelImplCopyWith<_$UserApiModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/compass_app/app/lib/data/services/api/model/user/user_api_model.g.dart b/compass_app/app/lib/data/services/api/model/user/user_api_model.g.dart new file mode 100644 index 00000000000..b40cb814a2a --- /dev/null +++ b/compass_app/app/lib/data/services/api/model/user/user_api_model.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_api_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$UserApiModelImpl _$$UserApiModelImplFromJson(Map json) => + _$UserApiModelImpl( + id: json['id'] as String, + name: json['name'] as String, + email: json['email'] as String, + picture: json['picture'] as String, + ); + +Map _$$UserApiModelImplToJson(_$UserApiModelImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'email': instance.email, + 'picture': instance.picture, + }; diff --git a/compass_app/app/lib/data/services/local/local_data_service.dart b/compass_app/app/lib/data/services/local/local_data_service.dart index 491067d7dd4..d2b0c9565f8 100644 --- a/compass_app/app/lib/data/services/local/local_data_service.dart +++ b/compass_app/app/lib/data/services/local/local_data_service.dart @@ -6,6 +6,7 @@ import '../../../config/assets.dart'; import '../../../domain/models/activity/activity.dart'; import '../../../domain/models/continent/continent.dart'; import '../../../domain/models/destination/destination.dart'; +import '../../../domain/models/user/user.dart'; class LocalDataService { List getContinents() { @@ -55,4 +56,12 @@ class LocalDataService { final localData = await rootBundle.loadString(asset); return (jsonDecode(localData) as List).cast>(); } + + User getUser() { + return const User( + name: 'Sofie', + // For demo purposes we use a local asset + picture: 'assets/user.jpg', + ); + } } diff --git a/compass_app/app/lib/domain/models/booking/booking_summary.freezed.dart b/compass_app/app/lib/domain/models/booking/booking_summary.freezed.dart index 59686266e95..ddfe78ae706 100644 --- a/compass_app/app/lib/domain/models/booking/booking_summary.freezed.dart +++ b/compass_app/app/lib/domain/models/booking/booking_summary.freezed.dart @@ -23,10 +23,10 @@ mixin _$BookingSummary { /// Booking id int get id => throw _privateConstructorUsedError; - /// Destination name to be displayed. + /// Name to be displayed String get name => throw _privateConstructorUsedError; - /// Start date of the booking. + /// Start date of the booking DateTime get startDate => throw _privateConstructorUsedError; /// End date of the booking @@ -158,11 +158,11 @@ class _$BookingSummaryImpl implements _BookingSummary { @override final int id; - /// Destination name to be displayed. + /// Name to be displayed @override final String name; - /// Start date of the booking. + /// Start date of the booking @override final DateTime startDate; @@ -222,11 +222,11 @@ abstract class _BookingSummary implements BookingSummary { @override int get id; - /// Destination name to be displayed. + /// Name to be displayed @override String get name; - /// Start date of the booking. + /// Start date of the booking @override DateTime get startDate; diff --git a/compass_app/app/lib/domain/models/user/user.dart b/compass_app/app/lib/domain/models/user/user.dart new file mode 100644 index 00000000000..d369098d157 --- /dev/null +++ b/compass_app/app/lib/domain/models/user/user.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user.freezed.dart'; +part 'user.g.dart'; + +@freezed +abstract class User with _$User { + const factory User({ + /// The user's name. + required String name, + + /// The user's picture URL. + required String picture, + }) = _User; + + factory User.fromJson(Map json) => _$UserFromJson(json); +} diff --git a/compass_app/app/lib/domain/models/user/user.freezed.dart b/compass_app/app/lib/domain/models/user/user.freezed.dart new file mode 100644 index 00000000000..214f6d8b4a6 --- /dev/null +++ b/compass_app/app/lib/domain/models/user/user.freezed.dart @@ -0,0 +1,185 @@ +// 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 'user.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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'); + +User _$UserFromJson(Map json) { + return _User.fromJson(json); +} + +/// @nodoc +mixin _$User { + /// The user's name. + String get name => throw _privateConstructorUsedError; + + /// The user's picture URL. + String get picture => throw _privateConstructorUsedError; + + /// Serializes this User to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserCopyWith<$Res> { + factory $UserCopyWith(User value, $Res Function(User) then) = + _$UserCopyWithImpl<$Res, User>; + @useResult + $Res call({String name, String picture}); +} + +/// @nodoc +class _$UserCopyWithImpl<$Res, $Val extends User> + implements $UserCopyWith<$Res> { + _$UserCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? picture = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + picture: null == picture + ? _value.picture + : picture // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> { + factory _$$UserImplCopyWith( + _$UserImpl value, $Res Function(_$UserImpl) then) = + __$$UserImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, String picture}); +} + +/// @nodoc +class __$$UserImplCopyWithImpl<$Res> + extends _$UserCopyWithImpl<$Res, _$UserImpl> + implements _$$UserImplCopyWith<$Res> { + __$$UserImplCopyWithImpl(_$UserImpl _value, $Res Function(_$UserImpl) _then) + : super(_value, _then); + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? picture = null, + }) { + return _then(_$UserImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + picture: null == picture + ? _value.picture + : picture // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserImpl implements _User { + const _$UserImpl({required this.name, required this.picture}); + + factory _$UserImpl.fromJson(Map json) => + _$$UserImplFromJson(json); + + /// The user's name. + @override + final String name; + + /// The user's picture URL. + @override + final String picture; + + @override + String toString() { + return 'User(name: $name, picture: $picture)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.picture, picture) || other.picture == picture)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, name, picture); + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserImplCopyWith<_$UserImpl> get copyWith => + __$$UserImplCopyWithImpl<_$UserImpl>(this, _$identity); + + @override + Map toJson() { + return _$$UserImplToJson( + this, + ); + } +} + +abstract class _User implements User { + const factory _User( + {required final String name, required final String picture}) = _$UserImpl; + + factory _User.fromJson(Map json) = _$UserImpl.fromJson; + + /// The user's name. + @override + String get name; + + /// The user's picture URL. + @override + String get picture; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserImplCopyWith<_$UserImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/compass_app/app/lib/domain/models/user/user.g.dart b/compass_app/app/lib/domain/models/user/user.g.dart new file mode 100644 index 00000000000..5493bf82222 --- /dev/null +++ b/compass_app/app/lib/domain/models/user/user.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$UserImpl _$$UserImplFromJson(Map json) => _$UserImpl( + name: json['name'] as String, + picture: json['picture'] as String, + ); + +Map _$$UserImplToJson(_$UserImpl instance) => + { + 'name': instance.name, + 'picture': instance.picture, + }; diff --git a/compass_app/app/lib/routing/router.dart b/compass_app/app/lib/routing/router.dart index a3492203781..42dba2f3092 100644 --- a/compass_app/app/lib/routing/router.dart +++ b/compass_app/app/lib/routing/router.dart @@ -45,6 +45,7 @@ GoRouter router( builder: (context, state) { final viewModel = HomeViewModel( bookingRepository: context.read(), + userRepository: context.read(), ); return HomeScreen(viewModel: viewModel); }, diff --git a/compass_app/app/lib/ui/core/localization/applocalization.dart b/compass_app/app/lib/ui/core/localization/applocalization.dart index 9338a0bc28f..2697be01387 100644 --- a/compass_app/app/lib/ui/core/localization/applocalization.dart +++ b/compass_app/app/lib/ui/core/localization/applocalization.dart @@ -19,6 +19,7 @@ class AppLocalization { 'errorWhileLoadingBooking': 'Error while loading booking', 'errorWhileLoadingContinents': 'Error while loading continents', 'errorWhileLoadingDestinations': 'Error while loading destinations', + 'errorWhileLoadingHome': 'Error while loading home', 'errorWhileLogin': 'Error while trying to login', 'errorWhileLogout': 'Error while trying to logout', 'errorWhileSavingActivities': 'Error while saving activities', @@ -26,12 +27,12 @@ class AppLocalization { 'errorWhileSharing': 'Error while sharing booking', 'evening': 'Evening', 'login': 'Login', + 'nameTrips': '{name}\'s Trips', 'search': 'Search', 'searchDestination': 'Search destination', 'selected': '{1} selected', 'shareTrip': 'Share Trip', 'tryAgain': 'Try again', - 'yourBookings': 'Your bookings:', 'yourChosenActivities': 'Your chosen activities', 'when': 'When', }; @@ -87,7 +88,9 @@ class AppLocalization { String get bookNewTrip => _get('bookNewTrip'); - String get yourBookings => _get('yourBookings'); + String get errorWhileLoadingHome => _get('errorWhileLoadingHome'); + + String nameTrips(String name) => _get('nameTrips').replaceAll('{name}', name); String selected(int value) => _get('selected').replaceAll('{1}', value.toString()); diff --git a/compass_app/app/lib/ui/core/themes/dimens.dart b/compass_app/app/lib/ui/core/themes/dimens.dart index d2411e864ea..4791bfbd2ea 100644 --- a/compass_app/app/lib/ui/core/themes/dimens.dart +++ b/compass_app/app/lib/ui/core/themes/dimens.dart @@ -32,6 +32,8 @@ sealed class Dimens { > 600 => dimensDesktop, _ => dimensMobile, }; + + abstract final double profilePictureSize; } /// Mobile dimensions @@ -41,6 +43,9 @@ class DimensMobile extends Dimens { @override double paddingScreenVertical = Dimens.paddingVertical; + + @override + double get profilePictureSize => 64.0; } /// Desktop/Web dimensions @@ -50,4 +55,7 @@ class DimensDesktop extends Dimens { @override double paddingScreenVertical = 64.0; + + @override + double get profilePictureSize => 128.0; } diff --git a/compass_app/app/lib/ui/home/view_models/home_viewmodel.dart b/compass_app/app/lib/ui/home/view_models/home_viewmodel.dart index e79429c3502..02925393c44 100644 --- a/compass_app/app/lib/ui/home/view_models/home_viewmodel.dart +++ b/compass_app/app/lib/ui/home/view_models/home_viewmodel.dart @@ -4,25 +4,33 @@ import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import '../../../data/repositories/booking/booking_repository.dart'; +import '../../../data/repositories/user/user_repository.dart'; import '../../../domain/models/booking/booking_summary.dart'; +import '../../../domain/models/user/user.dart'; import '../../../utils/command.dart'; import '../../../utils/result.dart'; class HomeViewModel extends ChangeNotifier { HomeViewModel({ required BookingRepository bookingRepository, - }) : _bookingRepository = bookingRepository { + required UserRepository userRepository, + }) : _bookingRepository = bookingRepository, + _userRepository = userRepository { load = Command0(_load)..execute(); } final BookingRepository _bookingRepository; + final UserRepository _userRepository; final _log = Logger('HomeViewModel'); List _bookings = []; + User? _user; late Command0 load; List get bookings => _bookings; + User? get user => _user; + Future _load() async { try { final result = await _bookingRepository.getBookingsList(); @@ -32,8 +40,19 @@ class HomeViewModel extends ChangeNotifier { _log.fine('Loaded bookings'); case Error>(): _log.warning('Failed to load bookings', result.error); + return result; + } + + final userResult = await _userRepository.getUser(); + switch (userResult) { + case Ok(): + _user = userResult.value; + _log.fine('Loaded user'); + case Error(): + _log.warning('Failed to load user', userResult.error); } - return result; + + return userResult; } finally { notifyListeners(); } diff --git a/compass_app/app/lib/ui/home/widgets/home_screen.dart b/compass_app/app/lib/ui/home/widgets/home_screen.dart index dc748839ddc..715c23c1119 100644 --- a/compass_app/app/lib/ui/home/widgets/home_screen.dart +++ b/compass_app/app/lib/ui/home/widgets/home_screen.dart @@ -1,15 +1,14 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; import '../../../domain/models/booking/booking_summary.dart'; import '../../../routing/routes.dart'; -import '../../auth/logout/view_models/logout_viewmodel.dart'; -import '../../auth/logout/widgets/logout_button.dart'; import '../../core/localization/applocalization.dart'; import '../../core/themes/dimens.dart'; import '../../core/ui/date_format_start_end.dart'; +import '../../core/ui/error_indicator.dart'; import '../view_models/home_viewmodel.dart'; +import 'home_title.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({ @@ -34,51 +33,58 @@ class HomeScreen extends StatelessWidget { top: true, bottom: true, child: ListenableBuilder( - listenable: viewModel, - builder: (context, _) { - return CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.symmetric( - vertical: Dimens.of(context).paddingScreenVertical, - horizontal: Dimens.of(context).paddingScreenHorizontal, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalization.of(context).yourBookings, - style: Theme.of(context).textTheme.headlineMedium, - ), - LogoutButton( - viewModel: LogoutViewModel( - authRepository: context.read(), - itineraryConfigRepository: context.read(), - ), - ), - ], + listenable: viewModel.load, + builder: (context, child) { + if (viewModel.load.running) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (viewModel.load.error) { + return ErrorIndicator( + title: AppLocalization.of(context).errorWhileLoadingHome, + label: AppLocalization.of(context).tryAgain, + onPressed: viewModel.load.execute, + ); + } + + return child!; + }, + child: ListenableBuilder( + listenable: viewModel, + builder: (context, _) { + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.symmetric( + vertical: Dimens.of(context).paddingScreenVertical, + horizontal: Dimens.of(context).paddingScreenHorizontal, + ), + child: HomeHeader(viewModel: viewModel), ), ), - ), - SliverList.builder( - itemCount: viewModel.bookings.length, - itemBuilder: (_, index) => _Booking( - key: ValueKey(index), - booking: viewModel.bookings[index], - onTap: () => context.push( - Routes.bookingWithId(viewModel.bookings[index].id)), - ), - ) - ], - ); - }, + SliverList.builder( + itemCount: viewModel.bookings.length, + itemBuilder: (_, index) => _Booking( + key: ValueKey(index), + booking: viewModel.bookings[index], + onTap: () => context.push( + Routes.bookingWithId(viewModel.bookings[index].id)), + ), + ) + ], + ); + }, + ), ), ), ); } } + class _Booking extends StatelessWidget { const _Booking({ super.key, diff --git a/compass_app/app/lib/ui/home/widgets/home_title.dart b/compass_app/app/lib/ui/home/widgets/home_title.dart new file mode 100644 index 00000000000..6132e870cc9 --- /dev/null +++ b/compass_app/app/lib/ui/home/widgets/home_title.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; + +import '../../auth/logout/view_models/logout_viewmodel.dart'; +import '../../auth/logout/widgets/logout_button.dart'; +import '../../core/localization/applocalization.dart'; +import '../../core/themes/dimens.dart'; +import '../view_models/home_viewmodel.dart'; + +class HomeHeader extends StatelessWidget { + const HomeHeader({ + super.key, + required this.viewModel, + }); + + final HomeViewModel viewModel; + + @override + Widget build(BuildContext context) { + final user = viewModel.user; + if (user == null) { + return const SizedBox(); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipOval( + child: Image.asset( + user.picture, + width: Dimens.of(context).profilePictureSize, + height: Dimens.of(context).profilePictureSize, + ), + ), + LogoutButton( + viewModel: LogoutViewModel( + authRepository: context.read(), + itineraryConfigRepository: context.read(), + ), + ), + ], + ), + const SizedBox(height: Dimens.paddingVertical), + _Title( + text: AppLocalization.of(context).nameTrips(user.name), + ), + ], + ); + } +} + +class _Title extends StatelessWidget { + const _Title({ + required this.text, + }); + + final String text; + + @override + Widget build(BuildContext context) { + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (bounds) => RadialGradient( + center: Alignment.bottomLeft, + radius: 2, + colors: [ + Colors.purple.shade700, + Colors.purple.shade400, + ], + ).createShader( + Rect.fromLTWH(0, 0, bounds.width, bounds.height), + ), + child: Text( + text, + style: GoogleFonts.rubik( + textStyle: Theme.of(context).textTheme.headlineLarge, + ), + ), + ); + } +} diff --git a/compass_app/app/pubspec.yaml b/compass_app/app/pubspec.yaml index c2aa37aae0e..dcf6b96a224 100644 --- a/compass_app/app/pubspec.yaml +++ b/compass_app/app/pubspec.yaml @@ -42,3 +42,4 @@ flutter: - assets/activities.json - assets/destinations.json - assets/logo.svg + - assets/user.jpg diff --git a/compass_app/app/test/ui/home/widgets/home_screen_test.dart b/compass_app/app/test/ui/home/widgets/home_screen_test.dart index b2f1c45de36..067e1df3c5a 100644 --- a/compass_app/app/test/ui/home/widgets/home_screen_test.dart +++ b/compass_app/app/test/ui/home/widgets/home_screen_test.dart @@ -12,6 +12,7 @@ import '../../../../testing/app.dart'; import '../../../../testing/fakes/repositories/fake_auth_repository.dart'; import '../../../../testing/fakes/repositories/fake_booking_repository.dart'; import '../../../../testing/fakes/repositories/fake_itinerary_config_repository.dart'; +import '../../../../testing/fakes/repositories/fake_user_repository.dart'; import '../../../../testing/mocks.dart'; import '../../../../testing/models/booking.dart'; @@ -23,6 +24,7 @@ void main() { setUp(() { viewModel = HomeViewModel( bookingRepository: FakeBookingRepository()..createBooking(kBooking), + userRepository: FakeUserRepository(), ); goRouter = MockGoRouter(); when(() => goRouter.push(any())).thenAnswer((_) => Future.value(null)); @@ -49,6 +51,13 @@ void main() { expect(find.byType(HomeScreen), findsOneWidget); }); + testWidgets('should show user name', (tester) async { + await loadWidget(tester); + await tester.pumpAndSettle(); + + expect(find.text('NAME\'s Trips'), findsOneWidget); + }); + testWidgets('should navigate to search', (tester) async { await loadWidget(tester); await tester.pumpAndSettle(); diff --git a/compass_app/app/testing/fakes/repositories/fake_user_repository.dart b/compass_app/app/testing/fakes/repositories/fake_user_repository.dart new file mode 100644 index 00000000000..b9d2de91541 --- /dev/null +++ b/compass_app/app/testing/fakes/repositories/fake_user_repository.dart @@ -0,0 +1,12 @@ +import 'package:compass_app/data/repositories/user/user_repository.dart'; +import 'package:compass_app/domain/models/user/user.dart'; +import 'package:compass_app/utils/result.dart'; + +import '../../models/user.dart'; + +class FakeUserRepository implements UserRepository { + @override + Future> getUser() async { + return Result.ok(user); + } +} diff --git a/compass_app/app/testing/fakes/services/fake_api_client.dart b/compass_app/app/testing/fakes/services/fake_api_client.dart index 4158a330405..e4b8486e175 100644 --- a/compass_app/app/testing/fakes/services/fake_api_client.dart +++ b/compass_app/app/testing/fakes/services/fake_api_client.dart @@ -1,5 +1,6 @@ import 'package:compass_app/data/services/api/api_client.dart'; import 'package:compass_app/data/services/api/model/booking/booking_api_model.dart'; +import 'package:compass_app/data/services/api/model/user/user_api_model.dart'; import 'package:compass_app/domain/models/activity/activity.dart'; import 'package:compass_app/domain/models/continent/continent.dart'; import 'package:compass_app/domain/models/destination/destination.dart'; @@ -7,6 +8,7 @@ import 'package:compass_app/utils/result.dart'; import '../../models/activity.dart'; import '../../models/booking.dart'; +import '../../models/user.dart'; class FakeApiClient implements ApiClient { // Should not increase when using cached data @@ -100,4 +102,9 @@ class FakeApiClient implements ApiClient { bookings.add(bookingWithId); return Result.ok(bookingWithId); } + + @override + Future> getUser() async { + return Result.ok(userApiModel); + } } diff --git a/compass_app/app/testing/models/user.dart b/compass_app/app/testing/models/user.dart new file mode 100644 index 00000000000..7be6d45958e --- /dev/null +++ b/compass_app/app/testing/models/user.dart @@ -0,0 +1,14 @@ +import 'package:compass_app/data/services/api/model/user/user_api_model.dart'; +import 'package:compass_app/domain/models/user/user.dart'; + +const userApiModel = UserApiModel( + id: 'ID', + name: 'NAME', + email: 'EMAIL', + picture: 'assets/user.jpg', +); + +const user = User( + name: 'NAME', + picture: 'assets/user.jpg', +); diff --git a/compass_app/server/bin/compass_server.dart b/compass_app/server/bin/compass_server.dart index 43593a8c1a4..73dad0d748e 100644 --- a/compass_app/server/bin/compass_server.dart +++ b/compass_app/server/bin/compass_server.dart @@ -5,6 +5,7 @@ import 'package:compass_server/routes/booking.dart'; import 'package:compass_server/routes/continent.dart'; import 'package:compass_server/routes/destination.dart'; import 'package:compass_server/routes/login.dart'; +import 'package:compass_server/routes/user.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart'; import 'package:shelf_router/shelf_router.dart'; @@ -14,6 +15,7 @@ final _router = Router() ..get('/continent', continentHandler) ..mount('/destination', DestinationApi().router.call) ..mount('/booking', BookingApi().router.call) + ..mount('/user', UserApi().router.call) ..mount('/login', LoginApi().router.call); void main(List args) async { diff --git a/compass_app/server/lib/config/constants.dart b/compass_app/server/lib/config/constants.dart index 4a9831b65ca..9ad1a236946 100644 --- a/compass_app/server/lib/config/constants.dart +++ b/compass_app/server/lib/config/constants.dart @@ -1,3 +1,5 @@ +import '../model/user/user.dart'; + class Constants { /// Email for the hardcoded login. static const email = 'email@example.com'; @@ -11,4 +13,18 @@ class Constants { /// User id to be returned on successful login. static const userId = '123'; + + /// User name for the hardcoded user. + static const name = 'Sofie'; + + /// For demo purposes we use a local asset. + static const picture = 'assets/user.jpg'; + + /// Hardcoded user. + static const user = User( + id: Constants.userId, + name: Constants.name, + email: Constants.email, + picture: Constants.picture, + ); } diff --git a/compass_app/server/lib/model/booking/booking.freezed.dart b/compass_app/server/lib/model/booking/booking.freezed.dart index 032c42d33b8..848015f738d 100644 --- a/compass_app/server/lib/model/booking/booking.freezed.dart +++ b/compass_app/server/lib/model/booking/booking.freezed.dart @@ -29,7 +29,7 @@ mixin _$Booking { /// End date of the trip DateTime get endDate => throw _privateConstructorUsedError; - /// Booking display name + /// Booking name /// Should be "Destination, Continent" String get name => throw _privateConstructorUsedError; @@ -205,7 +205,7 @@ class _$BookingImpl implements _Booking { @override final DateTime endDate; - /// Booking display name + /// Booking name /// Should be "Destination, Continent" @override final String name; @@ -290,7 +290,7 @@ abstract class _Booking implements Booking { @override DateTime get endDate; - /// Booking display name + /// Booking name /// Should be "Destination, Continent" @override String get name; diff --git a/compass_app/server/lib/model/user/user.dart b/compass_app/server/lib/model/user/user.dart new file mode 100644 index 00000000000..b11d25a87a7 --- /dev/null +++ b/compass_app/server/lib/model/user/user.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user.freezed.dart'; +part 'user.g.dart'; + +@freezed +abstract class User with _$User { + const factory User({ + /// The user's ID. + required String id, + + /// The user's name. + required String name, + + /// The user's email. + required String email, + + /// The user's picture URL. + required String picture, + }) = _User; + + factory User.fromJson(Map json) => _$UserFromJson(json); +} diff --git a/compass_app/server/lib/model/user/user.freezed.dart b/compass_app/server/lib/model/user/user.freezed.dart new file mode 100644 index 00000000000..a0e0ed628b3 --- /dev/null +++ b/compass_app/server/lib/model/user/user.freezed.dart @@ -0,0 +1,236 @@ +// 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 'user.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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'); + +User _$UserFromJson(Map json) { + return _User.fromJson(json); +} + +/// @nodoc +mixin _$User { + /// The user's ID. + String get id => throw _privateConstructorUsedError; + + /// The user's name. + String get name => throw _privateConstructorUsedError; + + /// The user's email. + String get email => throw _privateConstructorUsedError; + + /// The user's picture URL. + String get picture => throw _privateConstructorUsedError; + + /// Serializes this User to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserCopyWith<$Res> { + factory $UserCopyWith(User value, $Res Function(User) then) = + _$UserCopyWithImpl<$Res, User>; + @useResult + $Res call({String id, String name, String email, String picture}); +} + +/// @nodoc +class _$UserCopyWithImpl<$Res, $Val extends User> + implements $UserCopyWith<$Res> { + _$UserCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? email = null, + Object? picture = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + picture: null == picture + ? _value.picture + : picture // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> { + factory _$$UserImplCopyWith( + _$UserImpl value, $Res Function(_$UserImpl) then) = + __$$UserImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String id, String name, String email, String picture}); +} + +/// @nodoc +class __$$UserImplCopyWithImpl<$Res> + extends _$UserCopyWithImpl<$Res, _$UserImpl> + implements _$$UserImplCopyWith<$Res> { + __$$UserImplCopyWithImpl(_$UserImpl _value, $Res Function(_$UserImpl) _then) + : super(_value, _then); + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? email = null, + Object? picture = null, + }) { + return _then(_$UserImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + picture: null == picture + ? _value.picture + : picture // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserImpl implements _User { + const _$UserImpl( + {required this.id, + required this.name, + required this.email, + required this.picture}); + + factory _$UserImpl.fromJson(Map json) => + _$$UserImplFromJson(json); + + /// The user's ID. + @override + final String id; + + /// The user's name. + @override + final String name; + + /// The user's email. + @override + final String email; + + /// The user's picture URL. + @override + final String picture; + + @override + String toString() { + return 'User(id: $id, name: $name, email: $email, picture: $picture)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.picture, picture) || other.picture == picture)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, name, email, picture); + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserImplCopyWith<_$UserImpl> get copyWith => + __$$UserImplCopyWithImpl<_$UserImpl>(this, _$identity); + + @override + Map toJson() { + return _$$UserImplToJson( + this, + ); + } +} + +abstract class _User implements User { + const factory _User( + {required final String id, + required final String name, + required final String email, + required final String picture}) = _$UserImpl; + + factory _User.fromJson(Map json) = _$UserImpl.fromJson; + + /// The user's ID. + @override + String get id; + + /// The user's name. + @override + String get name; + + /// The user's email. + @override + String get email; + + /// The user's picture URL. + @override + String get picture; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserImplCopyWith<_$UserImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/compass_app/server/lib/model/user/user.g.dart b/compass_app/server/lib/model/user/user.g.dart new file mode 100644 index 00000000000..f131228469c --- /dev/null +++ b/compass_app/server/lib/model/user/user.g.dart @@ -0,0 +1,22 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$UserImpl _$$UserImplFromJson(Map json) => _$UserImpl( + id: json['id'] as String, + name: json['name'] as String, + email: json['email'] as String, + picture: json['picture'] as String, + ); + +Map _$$UserImplToJson(_$UserImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'email': instance.email, + 'picture': instance.picture, + }; diff --git a/compass_app/server/lib/routes/user.dart b/compass_app/server/lib/routes/user.dart new file mode 100644 index 00000000000..594136294df --- /dev/null +++ b/compass_app/server/lib/routes/user.dart @@ -0,0 +1,23 @@ +import 'dart:convert'; + +import 'package:compass_server/config/constants.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +/// Implements a simple user API. +/// +/// This API only returns a hardcoded user for demonstration purposes. +class UserApi { + Router get router { + final router = Router(); + + router.get('/', (Request request) async { + return Response.ok( + json.encode(Constants.user), + headers: {'Content-Type': 'application/json'}, + ); + }); + + return router; + } +} diff --git a/compass_app/server/test/server_test.dart b/compass_app/server/test/server_test.dart index d2d4d909080..54db0da755a 100644 --- a/compass_app/server/test/server_test.dart +++ b/compass_app/server/test/server_test.dart @@ -9,6 +9,7 @@ import 'package:compass_server/model/continent/continent.dart'; import 'package:compass_server/model/destination/destination.dart'; import 'package:compass_server/model/login_request/login_request.dart'; import 'package:compass_server/model/login_response/login_response.dart'; +import 'package:compass_server/model/user/user.dart'; import 'package:http/http.dart'; import 'package:test/test.dart'; @@ -128,6 +129,19 @@ void main() { expect(booking.id, 1); }); + test('Get user', () async { + final response = await get( + Uri.parse('$host/user'), + headers: headers, + ); + + expect(response.statusCode, 200); + final user = User.fromJson(jsonDecode(response.body)); + + // Should get the hardcoded user + expect(user, Constants.user); + }); + test('404', () async { final response = await get( Uri.parse('$host/foobar'),