refactor(legacy-account): migrate linked_devices to AsyncNotifier

This commit is contained in:
2026-04-21 21:47:36 +02:00
parent 0418f16f87
commit b6526f20ee
22 changed files with 559 additions and 1310 deletions

View File

@@ -1,7 +1,7 @@
import 'package:account/src/features/linked_devices/domain/entities/update_device_request_entity.dart';
import 'package:sf_shared/sf_shared.dart' show DeviceEntity;
abstract class DevicesRemoteDatasource {
Future<void> deleteDevice({required String deviceId});
Future<void> updateDevice({required UpdateDeviceRequestEntity request});
Future<void> updateDevice({required DeviceEntity device});
}

View File

@@ -1,6 +1,6 @@
import 'package:account/src/core/data/models/update_device_request_dto.dart';
import 'package:account/src/features/linked_devices/domain/entities/update_device_request_entity.dart';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/sf_shared.dart' show DeviceEntity;
import 'devices_remote_datasource.dart';
@@ -15,10 +15,11 @@ class DevicesRemoteDatasourceImpl implements DevicesRemoteDatasource {
}
@override
Future<void> updateDevice({
required UpdateDeviceRequestEntity request,
}) async {
final body = request.toDto().toJson();
await _repository.put<void>('/devices', body: body);
Future<void> updateDevice({required DeviceEntity device}) async {
final csvBase64 = DeviceCsvBuilder.buildBase64Csv(
device: device,
settings: device.settings,
);
await _repository.put<void>('/devices', body: {'csv': csvBase64});
}
}

View File

@@ -1,23 +0,0 @@
import 'package:account/src/features/linked_devices/domain/entities/update_device_request_entity.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'update_device_request_dto.freezed.dart';
part 'update_device_request_dto.g.dart';
@freezed
abstract class UpdateDeviceRequestDto with _$UpdateDeviceRequestDto {
const factory UpdateDeviceRequestDto({
required String identificator,
required String carrierName,
}) = _UpdateDeviceRequestDto;
factory UpdateDeviceRequestDto.fromJson(Map<String, dynamic> json) =>
_$UpdateDeviceRequestDtoFromJson(json);
}
extension UpdateDeviceRequestDtoMapper on UpdateDeviceRequestEntity {
UpdateDeviceRequestDto toDto() => UpdateDeviceRequestDto(
identificator: identificator,
carrierName: carrierName,
);
}

View File

@@ -1,280 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// 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 'update_device_request_dto.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$UpdateDeviceRequestDto {
String get identificator; String get carrierName;
/// Create a copy of UpdateDeviceRequestDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$UpdateDeviceRequestDtoCopyWith<UpdateDeviceRequestDto> get copyWith => _$UpdateDeviceRequestDtoCopyWithImpl<UpdateDeviceRequestDto>(this as UpdateDeviceRequestDto, _$identity);
/// Serializes this UpdateDeviceRequestDto to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is UpdateDeviceRequestDto&&(identical(other.identificator, identificator) || other.identificator == identificator)&&(identical(other.carrierName, carrierName) || other.carrierName == carrierName));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,identificator,carrierName);
@override
String toString() {
return 'UpdateDeviceRequestDto(identificator: $identificator, carrierName: $carrierName)';
}
}
/// @nodoc
abstract mixin class $UpdateDeviceRequestDtoCopyWith<$Res> {
factory $UpdateDeviceRequestDtoCopyWith(UpdateDeviceRequestDto value, $Res Function(UpdateDeviceRequestDto) _then) = _$UpdateDeviceRequestDtoCopyWithImpl;
@useResult
$Res call({
String identificator, String carrierName
});
}
/// @nodoc
class _$UpdateDeviceRequestDtoCopyWithImpl<$Res>
implements $UpdateDeviceRequestDtoCopyWith<$Res> {
_$UpdateDeviceRequestDtoCopyWithImpl(this._self, this._then);
final UpdateDeviceRequestDto _self;
final $Res Function(UpdateDeviceRequestDto) _then;
/// Create a copy of UpdateDeviceRequestDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? identificator = null,Object? carrierName = null,}) {
return _then(_self.copyWith(
identificator: null == identificator ? _self.identificator : identificator // ignore: cast_nullable_to_non_nullable
as String,carrierName: null == carrierName ? _self.carrierName : carrierName // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [UpdateDeviceRequestDto].
extension UpdateDeviceRequestDtoPatterns on UpdateDeviceRequestDto {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _UpdateDeviceRequestDto value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _UpdateDeviceRequestDto() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _UpdateDeviceRequestDto value) $default,){
final _that = this;
switch (_that) {
case _UpdateDeviceRequestDto():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _UpdateDeviceRequestDto value)? $default,){
final _that = this;
switch (_that) {
case _UpdateDeviceRequestDto() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identificator, String carrierName)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _UpdateDeviceRequestDto() when $default != null:
return $default(_that.identificator,_that.carrierName);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identificator, String carrierName) $default,) {final _that = this;
switch (_that) {
case _UpdateDeviceRequestDto():
return $default(_that.identificator,_that.carrierName);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identificator, String carrierName)? $default,) {final _that = this;
switch (_that) {
case _UpdateDeviceRequestDto() when $default != null:
return $default(_that.identificator,_that.carrierName);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _UpdateDeviceRequestDto implements UpdateDeviceRequestDto {
const _UpdateDeviceRequestDto({required this.identificator, required this.carrierName});
factory _UpdateDeviceRequestDto.fromJson(Map<String, dynamic> json) => _$UpdateDeviceRequestDtoFromJson(json);
@override final String identificator;
@override final String carrierName;
/// Create a copy of UpdateDeviceRequestDto
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$UpdateDeviceRequestDtoCopyWith<_UpdateDeviceRequestDto> get copyWith => __$UpdateDeviceRequestDtoCopyWithImpl<_UpdateDeviceRequestDto>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$UpdateDeviceRequestDtoToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UpdateDeviceRequestDto&&(identical(other.identificator, identificator) || other.identificator == identificator)&&(identical(other.carrierName, carrierName) || other.carrierName == carrierName));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,identificator,carrierName);
@override
String toString() {
return 'UpdateDeviceRequestDto(identificator: $identificator, carrierName: $carrierName)';
}
}
/// @nodoc
abstract mixin class _$UpdateDeviceRequestDtoCopyWith<$Res> implements $UpdateDeviceRequestDtoCopyWith<$Res> {
factory _$UpdateDeviceRequestDtoCopyWith(_UpdateDeviceRequestDto value, $Res Function(_UpdateDeviceRequestDto) _then) = __$UpdateDeviceRequestDtoCopyWithImpl;
@override @useResult
$Res call({
String identificator, String carrierName
});
}
/// @nodoc
class __$UpdateDeviceRequestDtoCopyWithImpl<$Res>
implements _$UpdateDeviceRequestDtoCopyWith<$Res> {
__$UpdateDeviceRequestDtoCopyWithImpl(this._self, this._then);
final _UpdateDeviceRequestDto _self;
final $Res Function(_UpdateDeviceRequestDto) _then;
/// Create a copy of UpdateDeviceRequestDto
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? identificator = null,Object? carrierName = null,}) {
return _then(_UpdateDeviceRequestDto(
identificator: null == identificator ? _self.identificator : identificator // ignore: cast_nullable_to_non_nullable
as String,carrierName: null == carrierName ? _self.carrierName : carrierName // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

View File

@@ -1,21 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'update_device_request_dto.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_UpdateDeviceRequestDto _$UpdateDeviceRequestDtoFromJson(
Map<String, dynamic> json,
) => _UpdateDeviceRequestDto(
identificator: json['identificator'] as String,
carrierName: json['carrierName'] as String,
);
Map<String, dynamic> _$UpdateDeviceRequestDtoToJson(
_UpdateDeviceRequestDto instance,
) => <String, dynamic>{
'identificator': instance.identificator,
'carrierName': instance.carrierName,
};

View File

@@ -1,7 +1,7 @@
import 'package:account/src/core/data/datasource/devices_remote_datasource.dart';
import 'package:account/src/features/linked_devices/domain/entities/update_device_request_entity.dart';
import 'package:dio/dio.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/sf_shared.dart' show DeviceEntity;
import '../../domain/repositories/devices_repository.dart';
@@ -20,9 +20,9 @@ class DevicesRepositoryImpl implements DevicesRepository {
}
@override
Future<void> updateDevice({required UpdateDeviceRequestEntity request}) async {
Future<void> updateDevice({required DeviceEntity device}) async {
try {
await _remote.updateDevice(request: request);
await _remote.updateDevice(device: device);
} on DioException catch (error) {
throw mapDioError(error, defaultMessage: 'Error to update device');
}

View File

@@ -1,7 +1,7 @@
import 'package:account/src/features/linked_devices/domain/entities/update_device_request_entity.dart';
import 'package:sf_shared/sf_shared.dart' show DeviceEntity;
abstract class DevicesRepository {
Future<void> deleteDevice({required String deviceId});
Future<void> updateDevice({required UpdateDeviceRequestEntity request});
Future<void> updateDevice({required DeviceEntity device});
}

View File

@@ -1,11 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'update_device_request_entity.freezed.dart';
@freezed
abstract class UpdateDeviceRequestEntity with _$UpdateDeviceRequestEntity {
const factory UpdateDeviceRequestEntity({
required String identificator,
required String carrierName,
}) = _UpdateDeviceRequestEntity;
}

View File

@@ -1,274 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// 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 'update_device_request_entity.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$UpdateDeviceRequestEntity {
String get identificator; String get carrierName;
/// Create a copy of UpdateDeviceRequestEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$UpdateDeviceRequestEntityCopyWith<UpdateDeviceRequestEntity> get copyWith => _$UpdateDeviceRequestEntityCopyWithImpl<UpdateDeviceRequestEntity>(this as UpdateDeviceRequestEntity, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is UpdateDeviceRequestEntity&&(identical(other.identificator, identificator) || other.identificator == identificator)&&(identical(other.carrierName, carrierName) || other.carrierName == carrierName));
}
@override
int get hashCode => Object.hash(runtimeType,identificator,carrierName);
@override
String toString() {
return 'UpdateDeviceRequestEntity(identificator: $identificator, carrierName: $carrierName)';
}
}
/// @nodoc
abstract mixin class $UpdateDeviceRequestEntityCopyWith<$Res> {
factory $UpdateDeviceRequestEntityCopyWith(UpdateDeviceRequestEntity value, $Res Function(UpdateDeviceRequestEntity) _then) = _$UpdateDeviceRequestEntityCopyWithImpl;
@useResult
$Res call({
String identificator, String carrierName
});
}
/// @nodoc
class _$UpdateDeviceRequestEntityCopyWithImpl<$Res>
implements $UpdateDeviceRequestEntityCopyWith<$Res> {
_$UpdateDeviceRequestEntityCopyWithImpl(this._self, this._then);
final UpdateDeviceRequestEntity _self;
final $Res Function(UpdateDeviceRequestEntity) _then;
/// Create a copy of UpdateDeviceRequestEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? identificator = null,Object? carrierName = null,}) {
return _then(_self.copyWith(
identificator: null == identificator ? _self.identificator : identificator // ignore: cast_nullable_to_non_nullable
as String,carrierName: null == carrierName ? _self.carrierName : carrierName // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [UpdateDeviceRequestEntity].
extension UpdateDeviceRequestEntityPatterns on UpdateDeviceRequestEntity {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _UpdateDeviceRequestEntity value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _UpdateDeviceRequestEntity() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _UpdateDeviceRequestEntity value) $default,){
final _that = this;
switch (_that) {
case _UpdateDeviceRequestEntity():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _UpdateDeviceRequestEntity value)? $default,){
final _that = this;
switch (_that) {
case _UpdateDeviceRequestEntity() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identificator, String carrierName)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _UpdateDeviceRequestEntity() when $default != null:
return $default(_that.identificator,_that.carrierName);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identificator, String carrierName) $default,) {final _that = this;
switch (_that) {
case _UpdateDeviceRequestEntity():
return $default(_that.identificator,_that.carrierName);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identificator, String carrierName)? $default,) {final _that = this;
switch (_that) {
case _UpdateDeviceRequestEntity() when $default != null:
return $default(_that.identificator,_that.carrierName);case _:
return null;
}
}
}
/// @nodoc
class _UpdateDeviceRequestEntity implements UpdateDeviceRequestEntity {
const _UpdateDeviceRequestEntity({required this.identificator, required this.carrierName});
@override final String identificator;
@override final String carrierName;
/// Create a copy of UpdateDeviceRequestEntity
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$UpdateDeviceRequestEntityCopyWith<_UpdateDeviceRequestEntity> get copyWith => __$UpdateDeviceRequestEntityCopyWithImpl<_UpdateDeviceRequestEntity>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UpdateDeviceRequestEntity&&(identical(other.identificator, identificator) || other.identificator == identificator)&&(identical(other.carrierName, carrierName) || other.carrierName == carrierName));
}
@override
int get hashCode => Object.hash(runtimeType,identificator,carrierName);
@override
String toString() {
return 'UpdateDeviceRequestEntity(identificator: $identificator, carrierName: $carrierName)';
}
}
/// @nodoc
abstract mixin class _$UpdateDeviceRequestEntityCopyWith<$Res> implements $UpdateDeviceRequestEntityCopyWith<$Res> {
factory _$UpdateDeviceRequestEntityCopyWith(_UpdateDeviceRequestEntity value, $Res Function(_UpdateDeviceRequestEntity) _then) = __$UpdateDeviceRequestEntityCopyWithImpl;
@override @useResult
$Res call({
String identificator, String carrierName
});
}
/// @nodoc
class __$UpdateDeviceRequestEntityCopyWithImpl<$Res>
implements _$UpdateDeviceRequestEntityCopyWith<$Res> {
__$UpdateDeviceRequestEntityCopyWithImpl(this._self, this._then);
final _UpdateDeviceRequestEntity _self;
final $Res Function(_UpdateDeviceRequestEntity) _then;
/// Create a copy of UpdateDeviceRequestEntity
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? identificator = null,Object? carrierName = null,}) {
return _then(_UpdateDeviceRequestEntity(
identificator: null == identificator ? _self.identificator : identificator // ignore: cast_nullable_to_non_nullable
as String,carrierName: null == carrierName ? _self.carrierName : carrierName // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

View File

@@ -1,28 +1,71 @@
import 'package:account/src/features/linked_devices/presentation/state/linked_devices_view_model.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:account/src/features/linked_devices/presentation/providers/linked_devices_controller.dart';
import 'package:account/src/features/linked_devices/presentation/providers/linked_devices_edit_mode_provider.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:legacy_ui/legacy_ui.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:utils/utils.dart';
class EditLinkedDeviceScreen extends ConsumerWidget {
const EditLinkedDeviceScreen({super.key});
class EditLinkedDeviceScreen extends ConsumerStatefulWidget {
final DeviceEntity device;
const EditLinkedDeviceScreen({super.key, required this.device});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<EditLinkedDeviceScreen> createState() =>
_EditLinkedDeviceScreenState();
}
final vm = ref.read(linkedDevicesViewModelProvider.notifier);
final device = ref.watch(
linkedDevicesViewModelProvider.select((s) => s.selectedDevice),
class _EditLinkedDeviceScreenState
extends ConsumerState<EditLinkedDeviceScreen> {
late final TextEditingController _nameController;
@override
void initState() {
super.initState();
_nameController = TextEditingController(
text: widget.device.carrierName ?? '',
);
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
void _onSubmit() {
if (_nameController.text.trim().isEmpty) return;
ref.read(linkedDevicesControllerProvider.notifier).updateDevice(
device: widget.device,
newName: _nameController.text,
);
}
@override
Widget build(BuildContext context) {
ref.listen(linkedDevicesControllerProvider, (prev, next) async {
next.showErrorOn(context);
if (prev != null &&
prev.isLoading &&
!next.isLoading &&
!next.hasError) {
await showSuccessDialog(context, I18n.deviceUpdatedSuccess);
if (context.mounted) {
ref.read(linkedDevicesEditModeProvider.notifier).disable();
Navigator.of(context).pop();
}
}
});
final submitState = ref.watch(linkedDevicesControllerProvider);
return LegacyPageLayout(
title: context.translate(I18n.editDeviceTitle),
showEdit: true,
onEditChange: vm.toggleIsEditing,
body: Column(
children: [
SizedBox(height: SizeUtils.getByScreen(small: 20, big: 18)),
@@ -33,100 +76,65 @@ class EditLinkedDeviceScreen extends ConsumerWidget {
big: EdgeInsets.symmetric(horizontal: 47, vertical: 8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
Stack(
children: [
Stack(
children: [
DecoratedBox(
decoration: BoxDecoration(color: Colors.blueAccent),
),
Center(
child: SvgPicture.asset(
'assets/shared/images/profile.svg',
),
),
Center(
child: SizedBox(
width: 160,
height: 160,
child: Align(
alignment: Alignment.bottomRight,
child: IconButton(
onPressed: () {},
icon: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFFCAC9C9),
),
padding: EdgeInsets.all(8),
child: Icon(
Icons.edit_outlined,
color: Colors.white,
size: SizeUtils.getByScreen(
small: 32,
big: 30,
),
),
const DecoratedBox(
decoration: BoxDecoration(color: Colors.blueAccent),
),
Center(
child: SvgPicture.asset(
'assets/shared/images/profile.svg',
),
),
Center(
child: SizedBox(
width: 160,
height: 160,
child: Align(
alignment: Alignment.bottomRight,
child: IconButton(
onPressed: () {},
icon: Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFFCAC9C9),
),
padding: const EdgeInsets.all(8),
child: Icon(
Icons.edit_outlined,
color: Colors.white,
size: SizeUtils.getByScreen(
small: 32,
big: 30,
),
),
),
),
),
],
),
SizedBox(
height: SizeUtils.getByScreen(small: 24, big: 22),
),
CustomTextField(
controller: vm.deviceNameController,
hint: device!.carrierName!,
label: context.translate(I18n.name),
),
),
],
),
SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)),
CustomTextField(
controller: _nameController,
hint: widget.device.carrierName ?? '',
label: context.translate(I18n.name),
),
],
),
),
),
],
),
footer: _SaveSection(),
);
}
}
class _SaveSection extends ConsumerWidget {
const _SaveSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(linkedDevicesViewModelProvider.notifier);
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: PrimaryButton(
onPressed: () async {
await vm.updateDevice();
if (!context.mounted) return;
final errorMessage = ref.read(
linkedDevicesViewModelProvider.select((s) => s.errorMessage),
);
if (errorMessage.isNotEmpty) {
showTopSnackbar(
context,
message: errorMessage,
type: MessageType.error,
);
}
},
text: context.translate(I18n.save),
color: context.sfColors.legacyPrimary,
footer: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: PrimaryButton(
onPressed: submitState.isLoading ? null : _onSubmit,
text: context.translate(I18n.save),
color: context.sfColors.legacyPrimary,
),
),
);
}

View File

@@ -1,10 +1,11 @@
import 'package:account/src/features/linked_devices/presentation/edit_linked_device_screen.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:account/src/features/linked_devices/presentation/state/linked_devices_view_model.dart';
import 'package:account/src/features/linked_devices/presentation/providers/linked_devices_controller.dart';
import 'package:account/src/features/linked_devices/presentation/providers/linked_devices_edit_mode_provider.dart';
import 'package:account/src/features/linked_devices/presentation/widgets/delete_device_dialog.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:legacy_ui/legacy_ui.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
@@ -18,60 +19,66 @@ class LinkedDevicesScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(linkedDevicesViewModelProvider.notifier);
final state = ref.watch(linkedDevicesViewModelProvider);
final devicesAsync = ref.watch(legacyDevicesProvider);
final isEditing = ref.watch(linkedDevicesEditModeProvider);
final toggleEditing = ref
.read(linkedDevicesEditModeProvider.notifier)
.toggle;
ref.listen(linkedDevicesControllerProvider, (prev, next) async {
next.showErrorOn(context);
if (prev != null &&
prev.isLoading &&
!next.isLoading &&
!next.hasError) {
await showSuccessDialog(context, I18n.deviceDeletedSuccess);
if (!context.mounted) return;
ref.read(linkedDevicesEditModeProvider.notifier).disable();
final remaining = ref.read(legacyDevicesProvider).value ?? const [];
if (remaining.isEmpty) {
navigationContract.goTo(AppRoutes.legacyDeviceSetup);
}
}
});
return LegacyPageLayout(
title: context.translate(I18n.linkedDevices),
showEdit: true,
onEditChange: vm.toggleIsEditing,
body: state.isLoading
? const Center(child: CircularProgressIndicator())
: Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 10, big: 12),
),
child: ListView.separated(
itemBuilder: (BuildContext context, int index) =>
_LinkedDeviceCard(
navigationContract: navigationContract,
device: state.linkedDevices[index],
isEditing: state.isEditing,
onDelete: () =>
vm.deleteDevice(state.linkedDevices[index]),
),
separatorBuilder: (BuildContext context, int index) =>
SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)),
itemCount: state.linkedDevices.length,
),
onEditChange: toggleEditing,
body: devicesAsync.when(
data: (devices) => Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 10, big: 12),
),
child: ListView.separated(
itemBuilder: (_, index) => _LinkedDeviceCard(
device: devices[index],
isEditing: isEditing,
),
separatorBuilder: (_, __) =>
SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)),
itemCount: devices.length,
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const SizedBox.shrink(),
),
);
}
}
class _LinkedDeviceCard extends ConsumerWidget {
final NavigationContract navigationContract;
class _LinkedDeviceCard extends StatelessWidget {
final DeviceEntity device;
final bool isEditing;
final Function onDelete;
const _LinkedDeviceCard({
required this.navigationContract,
required this.device,
required this.isEditing,
required this.onDelete,
});
const _LinkedDeviceCard({required this.device, required this.isEditing});
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(linkedDevicesViewModelProvider.notifier);
Widget build(BuildContext context) {
return Container(
padding: SizeUtils.getByScreen(
small: EdgeInsets.symmetric(horizontal: 22, vertical: 10),
big: EdgeInsets.symmetric(horizontal: 21, vertical: 8),
small: const EdgeInsets.symmetric(horizontal: 22, vertical: 10),
big: const EdgeInsets.symmetric(horizontal: 21, vertical: 8),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
@@ -100,7 +107,7 @@ class _LinkedDeviceCard extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
device.carrierName!,
device.carrierName ?? '',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 18, big: 19),
fontWeight: FontWeight.w500,
@@ -119,38 +126,30 @@ class _LinkedDeviceCard extends ConsumerWidget {
DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error,
borderRadius: BorderRadius.all(Radius.circular(12)),
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => Dialog(
child: DeleteDeviceDialog(
navigationContract: navigationContract,
device: device,
),
),
);
},
icon: Icon(Icons.close, color: Colors.white),
onPressed: () => showDialog<void>(
context: context,
builder: (_) => Dialog(child: DeleteDeviceDialog(device: device)),
),
icon: const Icon(Icons.close, color: Colors.white),
),
),
SizedBox(width: SizeUtils.getByScreen(small: 16, big: 14)),
DecoratedBox(
decoration: BoxDecoration(
color: context.sfColors.legacyPrimary,
borderRadius: BorderRadius.all(Radius.circular(12)),
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: IconButton(
onPressed: () {
vm.setSelectedDevice(device);
Navigator.push(
context,
MaterialPageRoute(builder: (_) => EditLinkedDeviceScreen()),
);
},
icon: Icon(Icons.edit_outlined, color: Colors.white),
onPressed: () => Navigator.push(
context,
MaterialPageRoute<void>(
builder: (_) => EditLinkedDeviceScreen(device: device),
),
),
icon: const Icon(Icons.edit_outlined, color: Colors.white),
),
),
],

View File

@@ -0,0 +1,51 @@
import 'dart:async';
import 'package:account/src/core/providers/devices_repository_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
part 'linked_devices_controller.g.dart';
@riverpod
class LinkedDevicesController extends _$LinkedDevicesController {
@override
FutureOr<void> build() {}
Future<void> deleteDevice(DeviceEntity device) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref
.read(devicesRepositoryProvider)
.deleteDevice(deviceId: device.id);
ref.read(legacyDevicesProvider.notifier).removeDevice(device.id);
final currentSelected = ref.read(selectedDeviceProvider).value;
if (currentSelected?.id == device.id) {
ref.invalidate(selectedDeviceProvider);
}
unawaited(
ref.read(sfTrackingProvider).legacyAccountLinkedDeviceUnlinked(),
);
});
}
Future<void> updateDevice({
required DeviceEntity device,
required String newName,
}) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final trimmed = newName.trim();
final updated = device.copyWith(carrierName: trimmed);
await ref.read(devicesRepositoryProvider).updateDevice(device: updated);
ref
.read(legacyDevicesProvider.notifier)
.renameDevice(deviceId: device.id, newCarrierName: trimmed);
unawaited(
ref.read(sfTrackingProvider).legacyAccountLinkedDeviceRenamed(),
);
});
}
}

View File

@@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'linked_devices_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(LinkedDevicesController)
const linkedDevicesControllerProvider = LinkedDevicesControllerProvider._();
final class LinkedDevicesControllerProvider
extends $AsyncNotifierProvider<LinkedDevicesController, void> {
const LinkedDevicesControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'linkedDevicesControllerProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$linkedDevicesControllerHash();
@$internal
@override
LinkedDevicesController create() => LinkedDevicesController();
}
String _$linkedDevicesControllerHash() =>
r'5840696cb5ad0e8dd1b8f671180af74eeb42ba9d';
abstract class _$LinkedDevicesController extends $AsyncNotifier<void> {
FutureOr<void> build();
@$mustCallSuper
@override
void runBuild() {
build();
final ref = this.ref as $Ref<AsyncValue<void>, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<void>, void>,
AsyncValue<void>,
Object?,
Object?
>;
element.handleValue(ref, null);
}
}

View File

@@ -0,0 +1,13 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'linked_devices_edit_mode_provider.g.dart';
@riverpod
class LinkedDevicesEditMode extends _$LinkedDevicesEditMode {
@override
bool build() => false;
void toggle() => state = !state;
void disable() => state = false;
}

View File

@@ -0,0 +1,64 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'linked_devices_edit_mode_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(LinkedDevicesEditMode)
const linkedDevicesEditModeProvider = LinkedDevicesEditModeProvider._();
final class LinkedDevicesEditModeProvider
extends $NotifierProvider<LinkedDevicesEditMode, bool> {
const LinkedDevicesEditModeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'linkedDevicesEditModeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$linkedDevicesEditModeHash();
@$internal
@override
LinkedDevicesEditMode create() => LinkedDevicesEditMode();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$linkedDevicesEditModeHash() =>
r'aea2da0d62667e721fa059fe5b48201689bbf9c2';
abstract class _$LinkedDevicesEditMode extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,132 +0,0 @@
import 'dart:async';
import 'package:account/src/features/linked_devices/domain/entities/update_device_request_entity.dart';
import 'package:account/src/features/linked_devices/presentation/state/linked_devices_view_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import '../../../../core/domain/repositories/devices_repository.dart';
import '../../../../core/providers/devices_repository_provider.dart';
final linkedDevicesViewModelProvider =
NotifierProvider.autoDispose<
LinkedDevicesViewModel,
LinkedDevicesViewState
>(LinkedDevicesViewModel.new);
class LinkedDevicesViewModel extends Notifier<LinkedDevicesViewState> {
late final SharedDevicesRepository _getDevicesRepository;
late final DevicesRepository _devicesRepository;
late final SfTrackingRepository _tracking;
late final TextEditingController deviceNameController;
@override
LinkedDevicesViewState build() {
_getDevicesRepository = ref.read(sharedDevicesRepositoryProvider);
_devicesRepository = ref.read(devicesRepositoryProvider);
_tracking = ref.read(sfTrackingProvider);
_initControllers();
_init();
return const LinkedDevicesViewState();
}
void _initControllers() {
deviceNameController = TextEditingController();
deviceNameController.addListener(_onDeviceNameChanged);
ref.onDispose(disposeControllers);
}
Future<void> _init() async {
final user = await ref.read(userInfoProvider.future);
state = state.copyWith(loggedUser: user);
final linkedDevices = await _getDevicesRepository.getDevices();
state = state.copyWith(linkedDevices: linkedDevices, isLoading: false);
}
void toggleIsEditing() {
state = state.copyWith(isEditing: !state.isEditing);
}
void _onDeviceNameChanged() {
final value = deviceNameController.text;
if (value == state.deviceName) return;
state = state.copyWith(deviceName: value);
}
void setSelectedDevice(DeviceEntity value) {
if (value == state.selectedDevice) return;
state = state.copyWith(selectedDevice: value);
}
Future<void> deleteDevice(DeviceEntity device) async {
try {
state = state.copyWith(isLoading: true, isComplete: false);
await _devicesRepository.deleteDevice(deviceId: device.id);
List<DeviceEntity> newList = state.linkedDevices.toList();
newList.remove(device);
if (device == state.selectedDevice) {
ref.invalidate(selectedDeviceProvider);
}
ref.read(legacyDevicesProvider.notifier).removeDevice(device.id);
unawaited(_tracking.legacyAccountLinkedDeviceUnlinked());
state = state.copyWith(
linkedDevices: newList,
isLoading: false,
isComplete: true,
);
} catch (e) {
state = state.copyWith(isLoading: false, errorMessage: e.toString());
return;
}
}
UpdateDeviceRequestEntity _toRequest(DeviceEntity device) {
return UpdateDeviceRequestEntity(
identificator: device.identificator,
carrierName: state.deviceName.trim(),
// phone: /*state.dialCode.trim() + */state.phoneNumber.trim(),
);
}
Future<void> updateDevice() async {
final deviceName = state.deviceName;
final device = state.selectedDevice!;
if (deviceName.isEmpty) return;
try {
await _devicesRepository.updateDevice(request: _toRequest(device));
ref.read(legacyDevicesProvider.notifier).renameDevice(
deviceId: device.id,
newCarrierName: deviceName.trim(),
);
unawaited(_tracking.legacyAccountLinkedDeviceRenamed());
} catch (e) {
state = state.copyWith(
isLoading: false,
isComplete: false,
errorMessage: e.toString(),
);
}
}
void disposeControllers() {
deviceNameController.removeListener(_onDeviceNameChanged);
deviceNameController.dispose();
}
}

View File

@@ -1,18 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:sf_shared/sf_shared.dart';
part 'linked_devices_view_state.freezed.dart';
@freezed
abstract class LinkedDevicesViewState with _$LinkedDevicesViewState {
const factory LinkedDevicesViewState({
@Default(true) bool isLoading,
@Default(true) bool isComplete,
UserEntity? loggedUser,
DeviceEntity? selectedDevice,
@Default([]) List<DeviceEntity> linkedDevices,
@Default(false) bool isEditing,
@Default('') String deviceName,
@Default('') String errorMessage,
}) = _LinkedDevicesViewState;
}

View File

@@ -1,346 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// 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 'linked_devices_view_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LinkedDevicesViewState {
bool get isLoading; bool get isComplete; UserEntity? get loggedUser; DeviceEntity? get selectedDevice; List<DeviceEntity> get linkedDevices; bool get isEditing; String get deviceName; String get errorMessage;
/// Create a copy of LinkedDevicesViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$LinkedDevicesViewStateCopyWith<LinkedDevicesViewState> get copyWith => _$LinkedDevicesViewStateCopyWithImpl<LinkedDevicesViewState>(this as LinkedDevicesViewState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LinkedDevicesViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.loggedUser, loggedUser) || other.loggedUser == loggedUser)&&(identical(other.selectedDevice, selectedDevice) || other.selectedDevice == selectedDevice)&&const DeepCollectionEquality().equals(other.linkedDevices, linkedDevices)&&(identical(other.isEditing, isEditing) || other.isEditing == isEditing)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,isComplete,loggedUser,selectedDevice,const DeepCollectionEquality().hash(linkedDevices),isEditing,deviceName,errorMessage);
@override
String toString() {
return 'LinkedDevicesViewState(isLoading: $isLoading, isComplete: $isComplete, loggedUser: $loggedUser, selectedDevice: $selectedDevice, linkedDevices: $linkedDevices, isEditing: $isEditing, deviceName: $deviceName, errorMessage: $errorMessage)';
}
}
/// @nodoc
abstract mixin class $LinkedDevicesViewStateCopyWith<$Res> {
factory $LinkedDevicesViewStateCopyWith(LinkedDevicesViewState value, $Res Function(LinkedDevicesViewState) _then) = _$LinkedDevicesViewStateCopyWithImpl;
@useResult
$Res call({
bool isLoading, bool isComplete, UserEntity? loggedUser, DeviceEntity? selectedDevice, List<DeviceEntity> linkedDevices, bool isEditing, String deviceName, String errorMessage
});
$UserEntityCopyWith<$Res>? get loggedUser;$DeviceEntityCopyWith<$Res>? get selectedDevice;
}
/// @nodoc
class _$LinkedDevicesViewStateCopyWithImpl<$Res>
implements $LinkedDevicesViewStateCopyWith<$Res> {
_$LinkedDevicesViewStateCopyWithImpl(this._self, this._then);
final LinkedDevicesViewState _self;
final $Res Function(LinkedDevicesViewState) _then;
/// Create a copy of LinkedDevicesViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? isComplete = null,Object? loggedUser = freezed,Object? selectedDevice = freezed,Object? linkedDevices = null,Object? isEditing = null,Object? deviceName = null,Object? errorMessage = null,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isComplete: null == isComplete ? _self.isComplete : isComplete // ignore: cast_nullable_to_non_nullable
as bool,loggedUser: freezed == loggedUser ? _self.loggedUser : loggedUser // ignore: cast_nullable_to_non_nullable
as UserEntity?,selectedDevice: freezed == selectedDevice ? _self.selectedDevice : selectedDevice // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,linkedDevices: null == linkedDevices ? _self.linkedDevices : linkedDevices // ignore: cast_nullable_to_non_nullable
as List<DeviceEntity>,isEditing: null == isEditing ? _self.isEditing : isEditing // ignore: cast_nullable_to_non_nullable
as bool,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of LinkedDevicesViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UserEntityCopyWith<$Res>? get loggedUser {
if (_self.loggedUser == null) {
return null;
}
return $UserEntityCopyWith<$Res>(_self.loggedUser!, (value) {
return _then(_self.copyWith(loggedUser: value));
});
}/// Create a copy of LinkedDevicesViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$DeviceEntityCopyWith<$Res>? get selectedDevice {
if (_self.selectedDevice == null) {
return null;
}
return $DeviceEntityCopyWith<$Res>(_self.selectedDevice!, (value) {
return _then(_self.copyWith(selectedDevice: value));
});
}
}
/// Adds pattern-matching-related methods to [LinkedDevicesViewState].
extension LinkedDevicesViewStatePatterns on LinkedDevicesViewState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LinkedDevicesViewState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _LinkedDevicesViewState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LinkedDevicesViewState value) $default,){
final _that = this;
switch (_that) {
case _LinkedDevicesViewState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LinkedDevicesViewState value)? $default,){
final _that = this;
switch (_that) {
case _LinkedDevicesViewState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, bool isComplete, UserEntity? loggedUser, DeviceEntity? selectedDevice, List<DeviceEntity> linkedDevices, bool isEditing, String deviceName, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LinkedDevicesViewState() when $default != null:
return $default(_that.isLoading,_that.isComplete,_that.loggedUser,_that.selectedDevice,_that.linkedDevices,_that.isEditing,_that.deviceName,_that.errorMessage);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, bool isComplete, UserEntity? loggedUser, DeviceEntity? selectedDevice, List<DeviceEntity> linkedDevices, bool isEditing, String deviceName, String errorMessage) $default,) {final _that = this;
switch (_that) {
case _LinkedDevicesViewState():
return $default(_that.isLoading,_that.isComplete,_that.loggedUser,_that.selectedDevice,_that.linkedDevices,_that.isEditing,_that.deviceName,_that.errorMessage);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, bool isComplete, UserEntity? loggedUser, DeviceEntity? selectedDevice, List<DeviceEntity> linkedDevices, bool isEditing, String deviceName, String errorMessage)? $default,) {final _that = this;
switch (_that) {
case _LinkedDevicesViewState() when $default != null:
return $default(_that.isLoading,_that.isComplete,_that.loggedUser,_that.selectedDevice,_that.linkedDevices,_that.isEditing,_that.deviceName,_that.errorMessage);case _:
return null;
}
}
}
/// @nodoc
class _LinkedDevicesViewState implements LinkedDevicesViewState {
const _LinkedDevicesViewState({this.isLoading = true, this.isComplete = true, this.loggedUser, this.selectedDevice, final List<DeviceEntity> linkedDevices = const [], this.isEditing = false, this.deviceName = '', this.errorMessage = ''}): _linkedDevices = linkedDevices;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isComplete;
@override final UserEntity? loggedUser;
@override final DeviceEntity? selectedDevice;
final List<DeviceEntity> _linkedDevices;
@override@JsonKey() List<DeviceEntity> get linkedDevices {
if (_linkedDevices is EqualUnmodifiableListView) return _linkedDevices;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_linkedDevices);
}
@override@JsonKey() final bool isEditing;
@override@JsonKey() final String deviceName;
@override@JsonKey() final String errorMessage;
/// Create a copy of LinkedDevicesViewState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$LinkedDevicesViewStateCopyWith<_LinkedDevicesViewState> get copyWith => __$LinkedDevicesViewStateCopyWithImpl<_LinkedDevicesViewState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LinkedDevicesViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.loggedUser, loggedUser) || other.loggedUser == loggedUser)&&(identical(other.selectedDevice, selectedDevice) || other.selectedDevice == selectedDevice)&&const DeepCollectionEquality().equals(other._linkedDevices, _linkedDevices)&&(identical(other.isEditing, isEditing) || other.isEditing == isEditing)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,isComplete,loggedUser,selectedDevice,const DeepCollectionEquality().hash(_linkedDevices),isEditing,deviceName,errorMessage);
@override
String toString() {
return 'LinkedDevicesViewState(isLoading: $isLoading, isComplete: $isComplete, loggedUser: $loggedUser, selectedDevice: $selectedDevice, linkedDevices: $linkedDevices, isEditing: $isEditing, deviceName: $deviceName, errorMessage: $errorMessage)';
}
}
/// @nodoc
abstract mixin class _$LinkedDevicesViewStateCopyWith<$Res> implements $LinkedDevicesViewStateCopyWith<$Res> {
factory _$LinkedDevicesViewStateCopyWith(_LinkedDevicesViewState value, $Res Function(_LinkedDevicesViewState) _then) = __$LinkedDevicesViewStateCopyWithImpl;
@override @useResult
$Res call({
bool isLoading, bool isComplete, UserEntity? loggedUser, DeviceEntity? selectedDevice, List<DeviceEntity> linkedDevices, bool isEditing, String deviceName, String errorMessage
});
@override $UserEntityCopyWith<$Res>? get loggedUser;@override $DeviceEntityCopyWith<$Res>? get selectedDevice;
}
/// @nodoc
class __$LinkedDevicesViewStateCopyWithImpl<$Res>
implements _$LinkedDevicesViewStateCopyWith<$Res> {
__$LinkedDevicesViewStateCopyWithImpl(this._self, this._then);
final _LinkedDevicesViewState _self;
final $Res Function(_LinkedDevicesViewState) _then;
/// Create a copy of LinkedDevicesViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? isComplete = null,Object? loggedUser = freezed,Object? selectedDevice = freezed,Object? linkedDevices = null,Object? isEditing = null,Object? deviceName = null,Object? errorMessage = null,}) {
return _then(_LinkedDevicesViewState(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isComplete: null == isComplete ? _self.isComplete : isComplete // ignore: cast_nullable_to_non_nullable
as bool,loggedUser: freezed == loggedUser ? _self.loggedUser : loggedUser // ignore: cast_nullable_to_non_nullable
as UserEntity?,selectedDevice: freezed == selectedDevice ? _self.selectedDevice : selectedDevice // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,linkedDevices: null == linkedDevices ? _self._linkedDevices : linkedDevices // ignore: cast_nullable_to_non_nullable
as List<DeviceEntity>,isEditing: null == isEditing ? _self.isEditing : isEditing // ignore: cast_nullable_to_non_nullable
as bool,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of LinkedDevicesViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UserEntityCopyWith<$Res>? get loggedUser {
if (_self.loggedUser == null) {
return null;
}
return $UserEntityCopyWith<$Res>(_self.loggedUser!, (value) {
return _then(_self.copyWith(loggedUser: value));
});
}/// Create a copy of LinkedDevicesViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$DeviceEntityCopyWith<$Res>? get selectedDevice {
if (_self.selectedDevice == null) {
return null;
}
return $DeviceEntityCopyWith<$Res>(_self.selectedDevice!, (value) {
return _then(_self.copyWith(selectedDevice: value));
});
}
}
// dart format on

View File

@@ -1,32 +1,23 @@
import 'package:account/src/features/linked_devices/presentation/state/linked_devices_view_model.dart';
import 'package:account/src/features/linked_devices/presentation/providers/linked_devices_controller.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:utils/utils.dart';
import 'package:legacy_theme/legacy_theme.dart';
class DeleteDeviceDialog extends ConsumerWidget {
final NavigationContract navigationContract;
final DeviceEntity device;
const DeleteDeviceDialog({
super.key,
required this.navigationContract,
required this.device,
});
const DeleteDeviceDialog({super.key, required this.device});
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(linkedDevicesViewModelProvider.notifier);
return Container(
padding: SizeUtils.getByScreen(
small: EdgeInsets.symmetric(horizontal: 32, vertical: 30),
big: EdgeInsets.symmetric(horizontal: 24, vertical: 18),
small: const EdgeInsets.symmetric(horizontal: 32, vertical: 30),
big: const EdgeInsets.symmetric(horizontal: 24, vertical: 18),
),
width: SizeUtils.getByScreen(small: 360, big: 350),
child: Column(
@@ -45,9 +36,7 @@ class DeleteDeviceDialog extends ConsumerWidget {
children: [
Expanded(
child: PrimaryButton(
onPressed: () {
Navigator.pop(context);
},
onPressed: () => Navigator.pop(context),
text: context.translate(I18n.cancel),
color: context.sfColors.legacyPrimary,
height: SizeUtils.getByScreen(small: 38, big: 36),
@@ -57,29 +46,11 @@ class DeleteDeviceDialog extends ConsumerWidget {
SizedBox(width: SizeUtils.getByScreen(small: 4, big: 16)),
Expanded(
child: PrimaryButton(
onPressed: () async {
await vm.deleteDevice(device);
if (!context.mounted) return;
final isComplete = ref.read(
linkedDevicesViewModelProvider.select(
(s) => s.isComplete,
),
);
if (isComplete) {
Navigator.pop(context);
}
final noMoreDevices = ref
.read(
linkedDevicesViewModelProvider.select(
(s) => s.linkedDevices,
),
)
.isEmpty;
if (noMoreDevices) {
navigationContract.goTo(AppRoutes.legacyDeviceSetup);
}
onPressed: () {
Navigator.pop(context);
ref
.read(linkedDevicesControllerProvider.notifier)
.deleteDevice(device);
},
text: context.translate(I18n.delete),
color: context.sfColors.legacyPrimary,

View File

@@ -0,0 +1,145 @@
import 'package:account/src/core/domain/repositories/devices_repository.dart';
import 'package:account/src/core/providers/devices_repository_provider.dart';
import 'package:account/src/features/linked_devices/presentation/providers/linked_devices_controller.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_shared/testing.dart';
import 'package:sf_tracking/sf_tracking.dart';
class MockDevicesRepository extends Mock implements DevicesRepository {}
class MockSharedDevicesRepository extends Mock
implements SharedDevicesRepository {}
const _deviceA = DeviceEntity(
id: 'device-A',
identificator: 'imei-A',
carrierName: 'Alice Watch',
phone: '+34600000001',
);
const _deviceB = DeviceEntity(
id: 'device-B',
identificator: 'imei-B',
carrierName: 'Bob Watch',
phone: '+34600000002',
);
void main() {
setUpAll(() {
registerFallbackValue(_deviceA);
});
ProviderContainer buildContainer({
required DevicesRepository devicesRepo,
List<DeviceEntity> initialDevices = const [_deviceA, _deviceB],
}) {
final shared = MockSharedDevicesRepository();
when(() => shared.getDevices()).thenAnswer((_) async => initialDevices);
return makeContainer(
overrides: [
sharedDevicesRepositoryProvider.overrideWithValue(shared),
devicesRepositoryProvider.overrideWithValue(devicesRepo),
sfTrackingProvider.overrideWithValue(
SfTrackingRepository(clients: const []),
),
],
);
}
group('LinkedDevicesController.deleteDevice', () {
test('removes device from legacyDevicesProvider on success', () async {
final repo = MockDevicesRepository();
when(() => repo.deleteDevice(deviceId: any(named: 'deviceId')))
.thenAnswer((_) async {});
final container = buildContainer(devicesRepo: repo);
addTearDown(container.dispose);
await container.read(legacyDevicesProvider.future);
await container
.read(linkedDevicesControllerProvider.notifier)
.deleteDevice(_deviceA);
final state = container.read(linkedDevicesControllerProvider);
expect(state, isA<AsyncData<void>>());
expect(state.error, isNull);
final remaining = container.read(legacyDevicesProvider).value;
expect(remaining, [_deviceB]);
verify(() => repo.deleteDevice(deviceId: 'device-A')).called(1);
});
test('exposes AsyncError when the repository fails', () async {
final repo = MockDevicesRepository();
when(() => repo.deleteDevice(deviceId: any(named: 'deviceId')))
.thenThrow(const ApiException(message: 'boom', isNetworkError: true));
final container = buildContainer(devicesRepo: repo);
addTearDown(container.dispose);
await container.read(legacyDevicesProvider.future);
await container
.read(linkedDevicesControllerProvider.notifier)
.deleteDevice(_deviceA);
final state = container.read(linkedDevicesControllerProvider);
expect(state, isA<AsyncError<void>>());
expect(state.error, isA<ApiException>());
});
});
group('LinkedDevicesController.updateDevice', () {
test('renames device in legacyDevicesProvider on success', () async {
final repo = MockDevicesRepository();
when(() => repo.updateDevice(device: any(named: 'device')))
.thenAnswer((_) async {});
final container = buildContainer(devicesRepo: repo);
addTearDown(container.dispose);
await container.read(legacyDevicesProvider.future);
await container
.read(linkedDevicesControllerProvider.notifier)
.updateDevice(device: _deviceA, newName: 'Alice Sport');
final state = container.read(linkedDevicesControllerProvider);
expect(state, isA<AsyncData<void>>());
final devices = container.read(legacyDevicesProvider).value!;
expect(devices.firstWhere((d) => d.id == 'device-A').carrierName,
'Alice Sport');
expect(devices.firstWhere((d) => d.id == 'device-B').carrierName,
'Bob Watch');
final captured = verify(
() => repo.updateDevice(device: captureAny(named: 'device')),
).captured.single as DeviceEntity;
expect(captured.id, 'device-A');
expect(captured.carrierName, 'Alice Sport');
});
test('exposes AsyncError when the repository fails', () async {
final repo = MockDevicesRepository();
when(() => repo.updateDevice(device: any(named: 'device')))
.thenThrow(const ApiException(message: 'boom', isNetworkError: true));
final container = buildContainer(devicesRepo: repo);
addTearDown(container.dispose);
await container.read(legacyDevicesProvider.future);
await container
.read(linkedDevicesControllerProvider.notifier)
.updateDevice(device: _deviceA, newName: 'Alice Sport');
final state = container.read(linkedDevicesControllerProvider);
expect(state, isA<AsyncError<void>>());
expect(state.error, isA<ApiException>());
});
});
}

View File

@@ -0,0 +1,47 @@
import 'package:account/src/features/linked_devices/presentation/providers/linked_devices_edit_mode_provider.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sf_shared/testing.dart';
void main() {
group('LinkedDevicesEditMode', () {
test('defaults to false', () {
final container = makeContainer();
addTearDown(container.dispose);
expect(container.read(linkedDevicesEditModeProvider), isFalse);
});
test('toggle flips the state', () {
final container = makeContainer();
addTearDown(container.dispose);
final notifier = container.read(
linkedDevicesEditModeProvider.notifier,
);
notifier.toggle();
expect(container.read(linkedDevicesEditModeProvider), isTrue);
notifier.toggle();
expect(container.read(linkedDevicesEditModeProvider), isFalse);
});
test('disable forces state to false regardless of current value', () {
final container = makeContainer();
addTearDown(container.dispose);
final notifier = container.read(
linkedDevicesEditModeProvider.notifier,
);
notifier.disable();
expect(container.read(linkedDevicesEditModeProvider), isFalse);
notifier.toggle();
expect(container.read(linkedDevicesEditModeProvider), isTrue);
notifier.disable();
expect(container.read(linkedDevicesEditModeProvider), isFalse);
});
});
}