delete device command and device setup flow

This commit is contained in:
2026-03-18 11:49:05 +01:00
parent fa36037aac
commit 990266ba95
19 changed files with 157 additions and 71 deletions

View File

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

View File

@@ -12,7 +12,7 @@ class DevicesRemoteDatasourceImpl implements DevicesRemoteDatasource {
final QuestiaRepository _repository;
@override
Future<void> deleteDevice({required String userId, required String deviceId}) async {
Future<void> deleteDevice({required String deviceId}) async {
try {
await _repository.delete<void>(
'/devices/$deviceId',
@@ -23,7 +23,7 @@ class DevicesRemoteDatasourceImpl implements DevicesRemoteDatasource {
}
@override
Future<void> updateDevice({required String userId, required UpdateDeviceRequestEntity request}) async {
Future<void> updateDevice({required UpdateDeviceRequestEntity request}) async {
try {
final body = request.toModel().toJson();
await _repository.put<void>(

View File

@@ -9,12 +9,12 @@ class DevicesRepositoryImpl implements DevicesRepository {
final DevicesRemoteDatasource _remote;
@override
Future<void> deleteDevice({required String userId, required String deviceId}) {
return _remote.deleteDevice(userId: userId, deviceId: deviceId);
Future<void> deleteDevice({required String deviceId}) {
return _remote.deleteDevice(deviceId: deviceId);
}
@override
Future<void> updateDevice({required String userId, required UpdateDeviceRequestEntity request}) {
return _remote.updateDevice(userId: userId, request: request);
Future<void> updateDevice({required UpdateDeviceRequestEntity request}) {
return _remote.updateDevice(request: request);
}
}

View File

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

View File

@@ -27,16 +27,20 @@ class LinkedDevicesScreen extends ConsumerWidget {
title: context.translate(I18n.linkedDevices),
showEdit: true,
onEditChange: vm.toggleIsEditing,
body: ListView.separated(
itemBuilder: (BuildContext context, int index)=>_LinkedDeviceCard(
device: state.linkedDevices[index],
isEditing: state.isEditing,
onDelete: ()=>vm.deleteDevice(state.linkedDevices[index]),
body: 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
),
separatorBuilder: (BuildContext context, int index)=>SizedBox(
height: SizeUtils.getByScreen(small: 18, big: 17)
),
itemCount: state.linkedDevices.length
),
);
}
@@ -44,11 +48,13 @@ class LinkedDevicesScreen extends ConsumerWidget {
class _LinkedDeviceCard extends ConsumerWidget {
final NavigationContract navigationContract;
final DeviceEntity device;
final bool isEditing;
final Function onDelete;
const _LinkedDeviceCard({
required this.navigationContract,
required this.device,
required this.isEditing,
required this.onDelete,
@@ -76,7 +82,7 @@ class _LinkedDeviceCard extends ConsumerWidget {
shape: BoxShape.circle,
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
padding: EdgeInsets.all(SizeUtils.getByScreen(small: 4, big: 12)),
padding: EdgeInsets.all(SizeUtils.getByScreen(small: 8, big: 12)),
child: Icon(SFIcons.watch,
size: SizeUtils.getByScreen(small: 40, big: 44),
color: theme.getColorFor(ThemeCode.legacyPrimary),
@@ -110,7 +116,10 @@ class _LinkedDeviceCard extends ConsumerWidget {
),
child: IconButton(
onPressed: (){showDialog(context: context, builder: (context)=>Dialog(
child: DeleteDeviceDialog(device: device),
child: DeleteDeviceDialog(
navigationContract: navigationContract,
device: device,
),
));},
icon: Icon(
Icons.close,

View File

@@ -70,19 +70,32 @@ class LinkedDevicesViewModel extends Notifier<LinkedDevicesViewState> {
);
}
Future<bool> deleteDevice(DeviceEntity device) async {
Future<void> deleteDevice(DeviceEntity device) async {
try {
await _devicesRepository.deleteDevice(userId: state.loggedUser!.id, deviceId: device.identificator);
List<DeviceEntity> newList = state.linkedDevices;
newList.remove(device);
state = state.copyWith(
linkedDevices: newList
isLoading: true,
isComplete: false,
);
return true;
await _devicesRepository.deleteDevice(deviceId: device.id);
List<DeviceEntity> newList = state.linkedDevices.toList();
newList.remove(device);
if (device == state.selectedDevice) {
ref.invalidate(selectedDeviceProvider);
}
state = state.copyWith(
linkedDevices: newList,
isLoading: false,
isComplete: true,
);
} catch (e) {
return false;
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
);
return;
}
}
@@ -102,9 +115,7 @@ class LinkedDevicesViewModel extends Notifier<LinkedDevicesViewState> {
if (deviceName.isEmpty) return;
try {
final userId = state.loggedUser!.id;
_devicesRepository.updateDevice(
userId: userId,
request: _toRequest(device));
} catch(e) {
@@ -114,8 +125,6 @@ class LinkedDevicesViewModel extends Notifier<LinkedDevicesViewState> {
errorMessage: e.toString()
);
}
}
void disposeControllers() {

View File

@@ -2,15 +2,20 @@ import 'package:account/src/features/linked_devices/presentation/state/linked_de
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:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:utils/utils.dart';
class DeleteDeviceDialog extends ConsumerWidget {
final NavigationContract navigationContract;
final DeviceEntity device;
const DeleteDeviceDialog({required this.device});
const DeleteDeviceDialog({
required this.navigationContract,
required this.device
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -21,15 +26,15 @@ class DeleteDeviceDialog extends ConsumerWidget {
return Container(
padding: SizeUtils.getByScreen(
small: EdgeInsets.symmetric(horizontal: 32, vertical: 30),
big: EdgeInsets.symmetric(horizontal: 30, vertical: 28)
big: EdgeInsets.symmetric(horizontal: 24, vertical: 18)
),
width: SizeUtils.getByScreen(small: 360, big: 350),
height: SizeUtils.getByScreen(small: 195, big: 185),
height: SizeUtils.getByScreen(small: 195, big: 160),
child: Column(
children: [
Text(context.translate(I18n.deleteDeviceDialog),
textAlign: TextAlign.center,
style: TextStyle(fontSize: SizeUtils.getByScreen(small: 19, big: 18)),
style: TextStyle(fontSize: SizeUtils.getByScreen(small: 19, big: 16)),
),
SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)),
Row(
@@ -46,7 +51,20 @@ class DeleteDeviceDialog extends ConsumerWidget {
Expanded(child: PrimaryButton(
onPressed: () async {
await vm.deleteDevice(device);
Navigator.pop(context);
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);
}
},
text: context.translate(I18n.delete),
color: theme.getColorFor(ThemeCode.legacyPrimary),

View File

@@ -8,6 +8,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation_contract.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
import 'widgets/activation_code_dialog.dart';
class LegacyDeviceSetupScreen extends ConsumerWidget {
final NavigationContract navigationContract;
@@ -54,7 +57,7 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 12, left: 8, right: 8),
padding: EdgeInsets.only(top: SizeUtils.getByScreen(small: 4, big: 12), left: 8, right: 8),
child: Row(
children: [
if (isIntro)
@@ -114,6 +117,9 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
}
return;
}
if (state.step == LegacyAddKidStep.scanWatch) {
showActivationCodeDialog(context);
}
await vm.next();
},
theme: theme,

View File

@@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
class LegacyIntroStepScreen extends ConsumerWidget {
const LegacyIntroStepScreen({super.key});
@@ -11,29 +12,34 @@ class LegacyIntroStepScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
context.translate(I18n.deviceSetup_intro_title),
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
const SizedBox(height: 30),
Text(
context.translate(I18n.deviceSetup_intro_subtitle),
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
SizedBox(height: 40),
LegacyNumberedSteps(
steps: [
context.translate(I18n.deviceSetup_intro_step_1),
context.translate(I18n.deviceSetup_intro_step_2),
],
color: theme.getColorFor(ThemeCode.legacyPrimary),
),
],
return Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 8, big: 2)
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
context.translate(I18n.deviceSetup_intro_title),
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
const SizedBox(height: 30),
Text(
context.translate(I18n.deviceSetup_intro_subtitle),
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
SizedBox(height: 40),
LegacyNumberedSteps(
steps: [
context.translate(I18n.deviceSetup_intro_step_1),
context.translate(I18n.deviceSetup_intro_step_2),
],
color: theme.getColorFor(ThemeCode.legacyPrimary),
),
],
),
);
}
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
class LegacyLinkInfoStepScreen extends ConsumerWidget {
const LegacyLinkInfoStepScreen({super.key});
@@ -13,7 +14,7 @@ class LegacyLinkInfoStepScreen extends ConsumerWidget {
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 30),
SizedBox(height: SizeUtils.getByScreen(small: 8, big: 30)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 65),
child: Text(

View File

@@ -147,7 +147,7 @@ class LegacyProfileStepScreen extends ConsumerWidget {
CustomTextField(
label: context.translate(I18n.activationKeyLabel),
hint: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
hint: 'XXXXXXXX',
controller: vm.activationKeyController,
),

View File

@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
void showActivationCodeDialog(BuildContext context) {
showDialog(context: context, builder: (context) =>
Dialog(
backgroundColor: Colors.transparent,
child: _ActivationCodeDialog(),
)
);
}
class _ActivationCodeDialog extends StatelessWidget {
const _ActivationCodeDialog();
@override
Widget build(BuildContext context) {
return Container(
padding: SizeUtils.getByScreen(
small: EdgeInsets.symmetric(horizontal: 14, vertical: 24),
big: EdgeInsets.symmetric(horizontal: 12, vertical: 20),
),
color: Colors.white,
child: Text(context.translate(I18n.activationCodeMessage),
textAlign: TextAlign.center,
style: TextStyle(fontSize: SizeUtils.getByScreen(small: 16, big: 15)),
),
);
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:legacy_auth/src/features/login/presentation/widgets/otp_code_fields.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:utils/utils.dart';
class LegacyTwoFactorBottomSheetView extends StatelessWidget {
const LegacyTwoFactorBottomSheetView({
@@ -88,6 +89,7 @@ class LegacyTwoFactorBottomSheetView extends StatelessWidget {
if (isOtpLoading || !_isValidOtp) return;
unawaited(onVerify());
},
gap: SizeUtils.getByScreen(small: 10, big: 8),
),
const SizedBox(height: 20),

View File

@@ -7,6 +7,6 @@
<versions>
<version>2.6.4</version>
</versions>
<lastUpdated>20260316000000</lastUpdated>
<lastUpdated>20260318000000</lastUpdated>
</versioning>
</metadata>

View File

@@ -1 +1 @@
a0ed8b315dd3aaa92839422686f00f9d
f9e85f64806f37132f0c0cc4ef8a67ec

View File

@@ -1 +1 @@
4888c373e3701bd965ce8ff0daaa19071f7d9a9e
b7a72907f1f917f7b7d0cd57cb170901965b4113

View File

@@ -582,7 +582,7 @@
"deviceSetup_weightHint": "30",
"deviceSetup_heightLabel": "Height (cm)",
"deviceSetup_heightHint": "120",
"activationKeyLabel": "Activation key",
"activationKeyLabel": "Activation key (check your email)",
"rewardsMessage": "*Using this feature you can reward your child for goals achieved or good actions.",
"sendRewards": "Send rewards!",
"rewardsSent": "Rewards sent!",
@@ -705,5 +705,6 @@
"wifiBssidHint": "e.g. 0c:80:63:e4:cb:e1",
"editChildProfile": "Edit profile",
"editChildProfileSaveSuccess": "Child profile updated successfully",
"editChildProfileTitle": "Edit child profile"
"editChildProfileTitle": "Edit child profile",
"activationCodeMessage": "An activation key has been sent to your email"
}

View File

@@ -580,7 +580,7 @@
"deviceSetup_weightHint": "30",
"deviceSetup_heightLabel": "Altura (cm)",
"deviceSetup_heightHint": "120",
"activationKeyLabel": "Clave de activación",
"activationKeyLabel": "Clave de activación (consulta tu correo electrónico)",
"rewardsMessage": "*Usando esta función puedes recompensar a tu hijo por metas alcanzadas o buenas acciones.",
"sendRewards": "¡Enviar recompensas!",
"rewardsSent": "¡Recompensas enviadas!",
@@ -703,5 +703,6 @@
"wifiBssidHint": "ej. 0c:80:63:e4:cb:e1",
"editChildProfile": "Editar perfil",
"editChildProfileTitle": "Editar perfil del niño",
"editChildProfileSaveSuccess": "Perfil del niño actualizado correctamente"
"editChildProfileSaveSuccess": "Perfil del niño actualizado correctamente",
"activationCodeMessage": "Se ha enviado un código de activación a tu correo electrónico"
}

View File

@@ -829,4 +829,5 @@ class I18n {
static const String editChildProfile = 'editChildProfile';
static const String editChildProfileTitle = 'editChildProfileTitle';
static const String editChildProfileSaveSuccess = 'editChildProfileSaveSuccess';
static const String activationCodeMessage = 'activationCodeMessage';
}