refactor(legacy_auth): migrate device_setup to Riverpod + polish QR scanner UX

This commit is contained in:
2026-04-23 00:16:05 +02:00
parent c23ae39b87
commit 6e6225d6b6
23 changed files with 1260 additions and 578 deletions

View File

@@ -1,16 +1,20 @@
import 'dart:async';
import 'package:legacy_auth/src/features/device_setup/presentation/state/device_setup_view_model.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/state/device_setup_view_state.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/enums/add_kid_step.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/step_body.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/success_screen.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/widgets/flow_footer.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_auth/legacy_auth.dart';
import 'package:legacy_auth/src/core/utils/date_format_utils.dart';
import 'package:legacy_auth/src/core/utils/text_format_utils.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/enums/add_kid_step.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_controller.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_form_controllers.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_state.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/step_body.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/success_screen.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/widgets/activation_code_dialog.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/widgets/flow_footer.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_localizations/sf_localizations.dart';
@@ -18,9 +22,7 @@ import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'package:utils/utils.dart';
import 'widgets/activation_code_dialog.dart';
class LegacyDeviceSetupScreen extends ConsumerWidget {
class LegacyDeviceSetupScreen extends ConsumerStatefulWidget {
final NavigationContract navigationContract;
final bool isFirstDevice;
@@ -31,26 +33,134 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(legacyDeviceSetupViewModelProvider);
final vm = ref.read(legacyDeviceSetupViewModelProvider.notifier);
ConsumerState<LegacyDeviceSetupScreen> createState() =>
_LegacyDeviceSetupScreenState();
}
class _LegacyDeviceSetupScreenState
extends ConsumerState<LegacyDeviceSetupScreen> {
late final DeviceSetupFormControllers _controllers;
@override
void initState() {
super.initState();
final initial = ref.read(deviceSetupControllerProvider);
_controllers = DeviceSetupFormControllers(
firstName: TextEditingController(text: initial.firstName),
lastName: TextEditingController(text: initial.lastName),
bornAt: TextEditingController(
text: initial.bornAt == null ? '' : formatDateDMY(initial.bornAt!),
),
weight: TextEditingController(text: initial.weight),
height: TextEditingController(text: initial.height),
watchCode: TextEditingController(text: initial.watchCode),
activationKey: TextEditingController(text: initial.activationKey),
);
_controllers.firstName.addListener(_onFirstNameChanged);
_controllers.lastName.addListener(_onLastNameChanged);
_controllers.bornAt.addListener(_onBornAtTextChanged);
_controllers.weight.addListener(_onWeightChanged);
_controllers.height.addListener(_onHeightChanged);
_controllers.watchCode.addListener(_onWatchCodeChanged);
_controllers.activationKey.addListener(_onActivationKeyChanged);
ref.listenManual(
deviceSetupControllerProvider.select((s) => s.bornAt),
(prev, next) {
if (next == prev) return;
final desired = next == null ? '' : formatDateDMY(next);
if (_controllers.bornAt.text != desired) {
_controllers.bornAt.text = desired;
}
},
);
}
@override
void dispose() {
_controllers.firstName.removeListener(_onFirstNameChanged);
_controllers.lastName.removeListener(_onLastNameChanged);
_controllers.bornAt.removeListener(_onBornAtTextChanged);
_controllers.weight.removeListener(_onWeightChanged);
_controllers.height.removeListener(_onHeightChanged);
_controllers.watchCode.removeListener(_onWatchCodeChanged);
_controllers.activationKey.removeListener(_onActivationKeyChanged);
_controllers.firstName.dispose();
_controllers.lastName.dispose();
_controllers.bornAt.dispose();
_controllers.weight.dispose();
_controllers.height.dispose();
_controllers.watchCode.dispose();
_controllers.activationKey.dispose();
super.dispose();
}
void _onFirstNameChanged() {
toCapitalizedController(_controllers.firstName);
ref
.read(deviceSetupControllerProvider.notifier)
.setFirstName(_controllers.firstName.text);
}
void _onLastNameChanged() {
toCapitalizedController(_controllers.lastName);
ref
.read(deviceSetupControllerProvider.notifier)
.setLastName(_controllers.lastName.text);
}
void _onBornAtTextChanged() {
final text = _controllers.bornAt.text;
final notifier = ref.read(deviceSetupControllerProvider.notifier);
if (text.trim().isEmpty) {
notifier.setBornAt(null);
return;
}
final parsed = tryParseDMY(text);
if (parsed != null) notifier.setBornAt(parsed);
}
void _onWeightChanged() {
ref
.read(deviceSetupControllerProvider.notifier)
.setWeight(_controllers.weight.text);
}
void _onHeightChanged() {
ref
.read(deviceSetupControllerProvider.notifier)
.setHeight(_controllers.height.text);
}
void _onWatchCodeChanged() {
ref
.read(deviceSetupControllerProvider.notifier)
.setWatchCode(_controllers.watchCode.text);
}
void _onActivationKeyChanged() {
toUpperCaseController(_controllers.activationKey);
ref
.read(deviceSetupControllerProvider.notifier)
.setActivationKey(_controllers.activationKey.text);
}
@override
Widget build(BuildContext context) {
final state = ref.watch(deviceSetupControllerProvider);
final notifier = ref.read(deviceSetupControllerProvider.notifier);
final isIntro = state.step == LegacyAddKidStep.intro;
final canPopRoute = state.step == LegacyAddKidStep.intro;
ref.listen(
legacyDeviceSetupViewModelProvider.select((s) => s.displayErrorKey),
(previous, next) {
if (next != null && next != previous) {
showTopSnackbar(
context,
message: context.translate(next),
type: MessageType.error,
);
vm.clearApiError();
vm.clearValidationError();
}
deviceSetupControllerProvider.select((s) => s.displayErrorKey),
(previous, next) async {
if (next == null || next == previous) return;
await showErrorDialog(context, next);
notifier.clearApiError();
notifier.clearValidationError();
},
);
@@ -58,7 +168,7 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
canPop: canPopRoute,
onPopInvokedWithResult: (didPop, result) {
if (didPop) return;
vm.back();
notifier.back();
},
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
@@ -73,10 +183,10 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
),
child: Row(
children: [
if (isIntro && isFirstDevice)
if (isIntro && widget.isFirstDevice)
IconButton(
onPressed: () => _confirmLogout(context, ref),
icon: Icon(Icons.logout),
icon: const Icon(Icons.logout),
color: Theme.of(context).colorScheme.onSurface,
)
else if (isIntro)
@@ -87,12 +197,11 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
)
else
IconButton(
onPressed: vm.back,
onPressed: notifier.back,
icon: Icon(Icons.adaptive.arrow_back),
color: Theme.of(context).colorScheme.onSurface,
tooltip: MaterialLocalizations.of(
context,
).backButtonTooltip,
tooltip:
MaterialLocalizations.of(context).backButtonTooltip,
),
Expanded(
child: isIntro
@@ -107,30 +216,31 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
],
),
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: LegacyStepBody(
key: ValueKey(state.step),
state: state,
step: state.step,
controllers: _controllers,
),
),
),
LegacyFlowFooter(
primaryText: context.translate(primaryButtonText(state.step)),
primaryText:
context.translate(_primaryButtonText(state.step)),
onPrimary: () async {
if (state.step == LegacyAddKidStep.profile) {
if (!vm.validateProfile()) return;
final ok = await vm.createDevice();
if (!notifier.validateProfile()) return;
final ok = await notifier.createDevice();
if (!context.mounted) return;
if (ok) {
if (isFirstDevice) {
if (widget.isFirstDevice) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => LegacySuccessScreen(
navigationContract: navigationContract,
navigationContract: widget.navigationContract,
formControllers: _controllers,
),
),
);
@@ -140,10 +250,15 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
}
return;
}
if (state.step == LegacyAddKidStep.scanWatch) {
final previousStep = state.step;
await notifier.next();
if (!context.mounted) return;
final advanced =
ref.read(deviceSetupControllerProvider).step !=
previousStep;
if (previousStep == LegacyAddKidStep.scanWatch && advanced) {
showActivationCodeDialog(context);
}
await vm.next();
},
),
],
@@ -156,7 +271,7 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
void _confirmLogout(BuildContext context, WidgetRef ref) {
final primaryColor = context.sfColors.legacyPrimary;
showDialog(
showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.translate(I18n.logOut)),
@@ -180,7 +295,7 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
ref.invalidate(selectedDeviceProvider);
unawaited(ref.read(sfTrackingProvider).legacyAuthLogout());
if (!context.mounted) return;
navigationContract.goTo(AppRoutes.legacyLogin);
widget.navigationContract.goTo(AppRoutes.legacyLogin);
},
child: Text(
context.translate(I18n.logOut),
@@ -192,7 +307,7 @@ class LegacyDeviceSetupScreen extends ConsumerWidget {
);
}
String primaryButtonText(LegacyAddKidStep step) {
String _primaryButtonText(LegacyAddKidStep step) {
switch (step) {
case LegacyAddKidStep.intro:
return I18n.deviceSetupStart;

View File

@@ -0,0 +1,276 @@
import 'dart:async';
import 'package:legacy_auth/src/core/providers/device_setup_repository_provider.dart';
import 'package:legacy_auth/src/features/device_setup/domain/entities/legacy_device_setup_error_event.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/enums/add_kid_step.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_state.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'package:utils/utils.dart';
part 'device_setup_controller.g.dart';
@Riverpod(keepAlive: true)
class DeviceSetupController extends _$DeviceSetupController {
DateTime? _currentStepEnteredAt;
@override
DeviceSetupState build() {
_currentStepEnteredAt = DateTime.now();
unawaited(ref.read(sfTrackingProvider).legacyDeviceSetupStarted());
return const DeviceSetupState();
}
void _completeCurrentStep(String stepName) {
final enteredAt = _currentStepEnteredAt;
if (enteredAt == null) return;
final duration = DateTime.now().difference(enteredAt).inSeconds;
unawaited(
ref.read(sfTrackingProvider).legacyDeviceSetupStepCompleted(
step: stepName,
durationSeconds: duration,
),
);
_currentStepEnteredAt = DateTime.now();
}
void setFirstName(String value) {
if (value == state.firstName) return;
state = state.copyWith(firstName: value, validationErrorKey: '');
}
void setLastName(String value) {
if (value == state.lastName) return;
state = state.copyWith(lastName: value, validationErrorKey: '');
}
void setBornAt(DateTime? date) {
if (date == state.bornAt) return;
state = state.copyWith(bornAt: date, validationErrorKey: '');
}
void setWeight(String value) {
if (value == state.weight) return;
state = state.copyWith(weight: value, validationErrorKey: '');
}
void setHeight(String value) {
if (value == state.height) return;
state = state.copyWith(height: value, validationErrorKey: '');
}
void setWatchCode(String value) {
if (value == state.watchCode) return;
state = state.copyWith(watchCode: value, validationErrorKey: '');
}
void setActivationKey(String value) {
if (value == state.activationKey) return;
state = state.copyWith(activationKey: value, validationErrorKey: '');
}
void onGenrerChanged(String? value) {
final v = value ?? '';
if (v == state.genrer) return;
state = state.copyWith(genrer: v, validationErrorKey: '');
}
void onRelationTypeChanged(String? value) {
final v = value ?? '';
if (v == state.relationType) return;
state = state.copyWith(relationType: v, validationErrorKey: '');
}
void setValidationError(String key) {
state = state.copyWith(validationErrorKey: key);
}
void clearApiError() {
if (state.apiErrorEvent != null) {
state = state.copyWith(apiErrorEvent: null);
}
}
void clearValidationError() {
if (state.validationErrorKey.isNotEmpty) {
state = state.copyWith(validationErrorKey: '');
}
}
Future<void> next() async {
switch (state.step) {
case LegacyAddKidStep.intro:
_completeCurrentStep('intro');
state = state.copyWith(step: LegacyAddKidStep.linkInfo);
return;
case LegacyAddKidStep.linkInfo:
_completeCurrentStep('link_info');
state = state.copyWith(step: LegacyAddKidStep.scanWatch);
return;
case LegacyAddKidStep.scanWatch:
final identificator = state.watchCode.trim();
if (identificator.isEmpty) {
state = state.copyWith(
validationErrorKey: I18n.errorScanWatchRequired,
);
return;
}
if (state.watchQr.isEmpty) {
unawaited(
ref
.read(sfTrackingProvider)
.legacyDeviceSetupManualCodeEntered(),
);
}
await _generateActivationKey(identificator);
return;
case LegacyAddKidStep.profile:
return;
}
}
Future<void> _generateActivationKey(String identificator) async {
state = state.copyWith(
isLoading: true,
validationErrorKey: '',
apiErrorEvent: null,
);
final tracking = ref.read(sfTrackingProvider);
try {
await ref
.read(legacyDeviceSetupRepositoryProvider)
.generateActivationKey(identificator: identificator);
_completeCurrentStep('scan_watch');
state =
state.copyWith(isLoading: false, step: LegacyAddKidStep.profile);
} catch (e) {
unawaited(
tracking.legacyDeviceSetupFailed(
atStep: 'scan_watch',
reason: e.toString(),
),
);
state = state.copyWith(
isLoading: false,
apiErrorEvent: mapActivationKeyError(e),
);
}
}
void back() {
final tracking = ref.read(sfTrackingProvider);
switch (state.step) {
case LegacyAddKidStep.intro:
return;
case LegacyAddKidStep.linkInfo:
unawaited(tracking.legacyDeviceSetupCancelled('link_info'));
state = state.copyWith(
step: LegacyAddKidStep.intro,
validationErrorKey: '',
);
return;
case LegacyAddKidStep.scanWatch:
unawaited(tracking.legacyDeviceSetupCancelled('scan_watch'));
state = state.copyWith(step: LegacyAddKidStep.linkInfo);
return;
case LegacyAddKidStep.profile:
unawaited(tracking.legacyDeviceSetupCancelled('profile'));
state = state.copyWith(step: LegacyAddKidStep.scanWatch);
return;
}
}
void onWatchQrScanned(String qr) {
unawaited(ref.read(sfTrackingProvider).legacyDeviceSetupQrScanned());
state = state.copyWith(watchQr: qr);
}
Future<bool> createDevice() async {
final name = '${state.firstName.trim()} ${state.lastName.trim()}'
.trim()
.toUpperCase();
final birth = state.bornAt!;
final bornAt = DateTime.utc(birth.year, birth.month, birth.day)
.millisecondsSinceEpoch;
final weight = int.parse(state.weight.trim());
final heightCm = double.parse(state.height.trim());
final stepLength = (heightCm * 0.40).round();
final genrer = state.genrer.trim();
final relationType = state.relationType.trim();
final activationKey = state.activationKey.trim();
state = state.copyWith(
isLoading: true,
apiErrorEvent: null,
validationErrorKey: '',
);
final tracking = ref.read(sfTrackingProvider);
try {
await ref.read(legacyDeviceSetupRepositoryProvider).createDevice(
name: name,
genrer: genrer,
weight: weight,
stepLength: stepLength,
bornAt: bornAt,
relationType: relationType,
activationKey: activationKey,
);
await ref.read(legacyDevicesProvider.notifier).refresh();
unawaited(
tracking.legacyDeviceSetupCompleted(
childGender: genrer,
relationType: relationType,
childAgeYears: yearsBetween(birth, DateTime.now()),
),
);
state = state.copyWith(isLoading: false, isSuccess: true);
return true;
} catch (e) {
unawaited(
tracking.legacyDeviceSetupFailed(
atStep: 'profile',
reason: e.toString(),
),
);
state = state.copyWith(
isLoading: false,
apiErrorEvent: mapActivateDeviceError(e),
);
return false;
}
}
bool validateProfile() {
final isInvalid = state.firstName.trim().isEmpty ||
state.lastName.trim().isEmpty ||
state.bornAt == null ||
state.genrer.trim().isEmpty ||
state.relationType.trim().isEmpty ||
state.weight.trim().isEmpty ||
int.tryParse(state.weight.trim()) == null ||
state.height.trim().isEmpty ||
double.tryParse(state.height.trim()) == null ||
state.activationKey.trim().isEmpty;
if (isInvalid) {
state = state.copyWith(validationErrorKey: I18n.errorAllFieldsRequired);
return false;
}
return true;
}
void resetForNewKid() {
unawaited(
ref.read(sfTrackingProvider).legacyDeviceSetupResetForNewKid(),
);
_currentStepEnteredAt = DateTime.now();
state = const DeviceSetupState();
}
}

View File

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

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class DeviceSetupFormControllers {
DeviceSetupFormControllers({
required this.firstName,
required this.lastName,
required this.bornAt,
required this.weight,
required this.height,
required this.watchCode,
required this.activationKey,
});
final TextEditingController firstName;
final TextEditingController lastName;
final TextEditingController bornAt;
final TextEditingController weight;
final TextEditingController height;
final TextEditingController watchCode;
final TextEditingController activationKey;
}

View File

@@ -1,13 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:legacy_auth/src/features/device_setup/domain/entities/legacy_device_setup_error_event.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/enums/add_kid_step.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:sf_localizations/sf_localizations.dart';
part 'device_setup_view_state.freezed.dart';
part 'device_setup_state.freezed.dart';
@freezed
abstract class LegacyDeviceSetupViewState with _$LegacyDeviceSetupViewState {
const factory LegacyDeviceSetupViewState({
abstract class DeviceSetupState with _$DeviceSetupState {
const factory DeviceSetupState({
@Default(LegacyAddKidStep.intro) LegacyAddKidStep step,
@Default('') String firstName,
@Default('') String lastName,
@@ -16,19 +16,17 @@ abstract class LegacyDeviceSetupViewState with _$LegacyDeviceSetupViewState {
@Default('') String relationType,
@Default('') String weight,
@Default('') String height,
@Default('') String watchQr,
@Default('') String watchCode,
@Default('') String activationKey,
@Default(false) bool isLoading,
@Default('') String validationErrorKey,
LegacyDeviceSetupErrorEvent? apiErrorEvent,
@Default(false) bool isSuccess,
}) = _AddKidFlowState;
}) = _DeviceSetupState;
}
extension LegacyDeviceSetupViewStateDisplay on LegacyDeviceSetupViewState {
extension DeviceSetupStateDisplay on DeviceSetupState {
String? get displayErrorKey {
if (validationErrorKey.isNotEmpty) return validationErrorKey;
final event = apiErrorEvent;

View File

@@ -3,7 +3,7 @@
// 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 'device_setup_view_state.dart';
part of 'device_setup_state.dart';
// **************************************************************************
// FreezedGenerator
@@ -12,20 +12,20 @@ part of 'device_setup_view_state.dart';
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LegacyDeviceSetupViewState {
mixin _$DeviceSetupState {
LegacyAddKidStep get step; String get firstName; String get lastName; DateTime? get bornAt; String get genrer; String get relationType; String get weight; String get height; String get watchQr; String get watchCode; String get activationKey; bool get isLoading; String get validationErrorKey; LegacyDeviceSetupErrorEvent? get apiErrorEvent; bool get isSuccess;
/// Create a copy of LegacyDeviceSetupViewState
/// Create a copy of DeviceSetupState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$LegacyDeviceSetupViewStateCopyWith<LegacyDeviceSetupViewState> get copyWith => _$LegacyDeviceSetupViewStateCopyWithImpl<LegacyDeviceSetupViewState>(this as LegacyDeviceSetupViewState, _$identity);
$DeviceSetupStateCopyWith<DeviceSetupState> get copyWith => _$DeviceSetupStateCopyWithImpl<DeviceSetupState>(this as DeviceSetupState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LegacyDeviceSetupViewState&&(identical(other.step, step) || other.step == step)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bornAt, bornAt) || other.bornAt == bornAt)&&(identical(other.genrer, genrer) || other.genrer == genrer)&&(identical(other.relationType, relationType) || other.relationType == relationType)&&(identical(other.weight, weight) || other.weight == weight)&&(identical(other.height, height) || other.height == height)&&(identical(other.watchQr, watchQr) || other.watchQr == watchQr)&&(identical(other.watchCode, watchCode) || other.watchCode == watchCode)&&(identical(other.activationKey, activationKey) || other.activationKey == activationKey)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.validationErrorKey, validationErrorKey) || other.validationErrorKey == validationErrorKey)&&(identical(other.apiErrorEvent, apiErrorEvent) || other.apiErrorEvent == apiErrorEvent)&&(identical(other.isSuccess, isSuccess) || other.isSuccess == isSuccess));
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceSetupState&&(identical(other.step, step) || other.step == step)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bornAt, bornAt) || other.bornAt == bornAt)&&(identical(other.genrer, genrer) || other.genrer == genrer)&&(identical(other.relationType, relationType) || other.relationType == relationType)&&(identical(other.weight, weight) || other.weight == weight)&&(identical(other.height, height) || other.height == height)&&(identical(other.watchQr, watchQr) || other.watchQr == watchQr)&&(identical(other.watchCode, watchCode) || other.watchCode == watchCode)&&(identical(other.activationKey, activationKey) || other.activationKey == activationKey)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.validationErrorKey, validationErrorKey) || other.validationErrorKey == validationErrorKey)&&(identical(other.apiErrorEvent, apiErrorEvent) || other.apiErrorEvent == apiErrorEvent)&&(identical(other.isSuccess, isSuccess) || other.isSuccess == isSuccess));
}
@@ -34,15 +34,15 @@ int get hashCode => Object.hash(runtimeType,step,firstName,lastName,bornAt,genre
@override
String toString() {
return 'LegacyDeviceSetupViewState(step: $step, firstName: $firstName, lastName: $lastName, bornAt: $bornAt, genrer: $genrer, relationType: $relationType, weight: $weight, height: $height, watchQr: $watchQr, watchCode: $watchCode, activationKey: $activationKey, isLoading: $isLoading, validationErrorKey: $validationErrorKey, apiErrorEvent: $apiErrorEvent, isSuccess: $isSuccess)';
return 'DeviceSetupState(step: $step, firstName: $firstName, lastName: $lastName, bornAt: $bornAt, genrer: $genrer, relationType: $relationType, weight: $weight, height: $height, watchQr: $watchQr, watchCode: $watchCode, activationKey: $activationKey, isLoading: $isLoading, validationErrorKey: $validationErrorKey, apiErrorEvent: $apiErrorEvent, isSuccess: $isSuccess)';
}
}
/// @nodoc
abstract mixin class $LegacyDeviceSetupViewStateCopyWith<$Res> {
factory $LegacyDeviceSetupViewStateCopyWith(LegacyDeviceSetupViewState value, $Res Function(LegacyDeviceSetupViewState) _then) = _$LegacyDeviceSetupViewStateCopyWithImpl;
abstract mixin class $DeviceSetupStateCopyWith<$Res> {
factory $DeviceSetupStateCopyWith(DeviceSetupState value, $Res Function(DeviceSetupState) _then) = _$DeviceSetupStateCopyWithImpl;
@useResult
$Res call({
LegacyAddKidStep step, String firstName, String lastName, DateTime? bornAt, String genrer, String relationType, String weight, String height, String watchQr, String watchCode, String activationKey, bool isLoading, String validationErrorKey, LegacyDeviceSetupErrorEvent? apiErrorEvent, bool isSuccess
@@ -53,14 +53,14 @@ $Res call({
}
/// @nodoc
class _$LegacyDeviceSetupViewStateCopyWithImpl<$Res>
implements $LegacyDeviceSetupViewStateCopyWith<$Res> {
_$LegacyDeviceSetupViewStateCopyWithImpl(this._self, this._then);
class _$DeviceSetupStateCopyWithImpl<$Res>
implements $DeviceSetupStateCopyWith<$Res> {
_$DeviceSetupStateCopyWithImpl(this._self, this._then);
final LegacyDeviceSetupViewState _self;
final $Res Function(LegacyDeviceSetupViewState) _then;
final DeviceSetupState _self;
final $Res Function(DeviceSetupState) _then;
/// Create a copy of LegacyDeviceSetupViewState
/// Create a copy of DeviceSetupState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? step = null,Object? firstName = null,Object? lastName = null,Object? bornAt = freezed,Object? genrer = null,Object? relationType = null,Object? weight = null,Object? height = null,Object? watchQr = null,Object? watchCode = null,Object? activationKey = null,Object? isLoading = null,Object? validationErrorKey = null,Object? apiErrorEvent = freezed,Object? isSuccess = null,}) {
return _then(_self.copyWith(
@@ -86,8 +86,8 @@ as bool,
}
/// Adds pattern-matching-related methods to [LegacyDeviceSetupViewState].
extension LegacyDeviceSetupViewStatePatterns on LegacyDeviceSetupViewState {
/// Adds pattern-matching-related methods to [DeviceSetupState].
extension DeviceSetupStatePatterns on DeviceSetupState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
@@ -100,10 +100,10 @@ extension LegacyDeviceSetupViewStatePatterns on LegacyDeviceSetupViewState {
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _AddKidFlowState value)? $default,{required TResult orElse(),}){
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _DeviceSetupState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _AddKidFlowState() when $default != null:
case _DeviceSetupState() when $default != null:
return $default(_that);case _:
return orElse();
@@ -122,10 +122,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _AddKidFlowState value) $default,){
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _DeviceSetupState value) $default,){
final _that = this;
switch (_that) {
case _AddKidFlowState():
case _DeviceSetupState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
@@ -143,10 +143,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _AddKidFlowState value)? $default,){
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _DeviceSetupState value)? $default,){
final _that = this;
switch (_that) {
case _AddKidFlowState() when $default != null:
case _DeviceSetupState() when $default != null:
return $default(_that);case _:
return null;
@@ -166,7 +166,7 @@ return $default(_that);case _:
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( LegacyAddKidStep step, String firstName, String lastName, DateTime? bornAt, String genrer, String relationType, String weight, String height, String watchQr, String watchCode, String activationKey, bool isLoading, String validationErrorKey, LegacyDeviceSetupErrorEvent? apiErrorEvent, bool isSuccess)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AddKidFlowState() when $default != null:
case _DeviceSetupState() when $default != null:
return $default(_that.step,_that.firstName,_that.lastName,_that.bornAt,_that.genrer,_that.relationType,_that.weight,_that.height,_that.watchQr,_that.watchCode,_that.activationKey,_that.isLoading,_that.validationErrorKey,_that.apiErrorEvent,_that.isSuccess);case _:
return orElse();
@@ -187,7 +187,7 @@ return $default(_that.step,_that.firstName,_that.lastName,_that.bornAt,_that.gen
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( LegacyAddKidStep step, String firstName, String lastName, DateTime? bornAt, String genrer, String relationType, String weight, String height, String watchQr, String watchCode, String activationKey, bool isLoading, String validationErrorKey, LegacyDeviceSetupErrorEvent? apiErrorEvent, bool isSuccess) $default,) {final _that = this;
switch (_that) {
case _AddKidFlowState():
case _DeviceSetupState():
return $default(_that.step,_that.firstName,_that.lastName,_that.bornAt,_that.genrer,_that.relationType,_that.weight,_that.height,_that.watchQr,_that.watchCode,_that.activationKey,_that.isLoading,_that.validationErrorKey,_that.apiErrorEvent,_that.isSuccess);case _:
throw StateError('Unexpected subclass');
@@ -207,7 +207,7 @@ return $default(_that.step,_that.firstName,_that.lastName,_that.bornAt,_that.gen
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( LegacyAddKidStep step, String firstName, String lastName, DateTime? bornAt, String genrer, String relationType, String weight, String height, String watchQr, String watchCode, String activationKey, bool isLoading, String validationErrorKey, LegacyDeviceSetupErrorEvent? apiErrorEvent, bool isSuccess)? $default,) {final _that = this;
switch (_that) {
case _AddKidFlowState() when $default != null:
case _DeviceSetupState() when $default != null:
return $default(_that.step,_that.firstName,_that.lastName,_that.bornAt,_that.genrer,_that.relationType,_that.weight,_that.height,_that.watchQr,_that.watchCode,_that.activationKey,_that.isLoading,_that.validationErrorKey,_that.apiErrorEvent,_that.isSuccess);case _:
return null;
@@ -219,8 +219,8 @@ return $default(_that.step,_that.firstName,_that.lastName,_that.bornAt,_that.gen
/// @nodoc
class _AddKidFlowState implements LegacyDeviceSetupViewState {
const _AddKidFlowState({this.step = LegacyAddKidStep.intro, this.firstName = '', this.lastName = '', this.bornAt, this.genrer = '', this.relationType = '', this.weight = '', this.height = '', this.watchQr = '', this.watchCode = '', this.activationKey = '', this.isLoading = false, this.validationErrorKey = '', this.apiErrorEvent, this.isSuccess = false});
class _DeviceSetupState implements DeviceSetupState {
const _DeviceSetupState({this.step = LegacyAddKidStep.intro, this.firstName = '', this.lastName = '', this.bornAt, this.genrer = '', this.relationType = '', this.weight = '', this.height = '', this.watchQr = '', this.watchCode = '', this.activationKey = '', this.isLoading = false, this.validationErrorKey = '', this.apiErrorEvent, this.isSuccess = false});
@override@JsonKey() final LegacyAddKidStep step;
@@ -239,17 +239,17 @@ class _AddKidFlowState implements LegacyDeviceSetupViewState {
@override final LegacyDeviceSetupErrorEvent? apiErrorEvent;
@override@JsonKey() final bool isSuccess;
/// Create a copy of LegacyDeviceSetupViewState
/// Create a copy of DeviceSetupState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AddKidFlowStateCopyWith<_AddKidFlowState> get copyWith => __$AddKidFlowStateCopyWithImpl<_AddKidFlowState>(this, _$identity);
_$DeviceSetupStateCopyWith<_DeviceSetupState> get copyWith => __$DeviceSetupStateCopyWithImpl<_DeviceSetupState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AddKidFlowState&&(identical(other.step, step) || other.step == step)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bornAt, bornAt) || other.bornAt == bornAt)&&(identical(other.genrer, genrer) || other.genrer == genrer)&&(identical(other.relationType, relationType) || other.relationType == relationType)&&(identical(other.weight, weight) || other.weight == weight)&&(identical(other.height, height) || other.height == height)&&(identical(other.watchQr, watchQr) || other.watchQr == watchQr)&&(identical(other.watchCode, watchCode) || other.watchCode == watchCode)&&(identical(other.activationKey, activationKey) || other.activationKey == activationKey)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.validationErrorKey, validationErrorKey) || other.validationErrorKey == validationErrorKey)&&(identical(other.apiErrorEvent, apiErrorEvent) || other.apiErrorEvent == apiErrorEvent)&&(identical(other.isSuccess, isSuccess) || other.isSuccess == isSuccess));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeviceSetupState&&(identical(other.step, step) || other.step == step)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bornAt, bornAt) || other.bornAt == bornAt)&&(identical(other.genrer, genrer) || other.genrer == genrer)&&(identical(other.relationType, relationType) || other.relationType == relationType)&&(identical(other.weight, weight) || other.weight == weight)&&(identical(other.height, height) || other.height == height)&&(identical(other.watchQr, watchQr) || other.watchQr == watchQr)&&(identical(other.watchCode, watchCode) || other.watchCode == watchCode)&&(identical(other.activationKey, activationKey) || other.activationKey == activationKey)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.validationErrorKey, validationErrorKey) || other.validationErrorKey == validationErrorKey)&&(identical(other.apiErrorEvent, apiErrorEvent) || other.apiErrorEvent == apiErrorEvent)&&(identical(other.isSuccess, isSuccess) || other.isSuccess == isSuccess));
}
@@ -258,15 +258,15 @@ int get hashCode => Object.hash(runtimeType,step,firstName,lastName,bornAt,genre
@override
String toString() {
return 'LegacyDeviceSetupViewState(step: $step, firstName: $firstName, lastName: $lastName, bornAt: $bornAt, genrer: $genrer, relationType: $relationType, weight: $weight, height: $height, watchQr: $watchQr, watchCode: $watchCode, activationKey: $activationKey, isLoading: $isLoading, validationErrorKey: $validationErrorKey, apiErrorEvent: $apiErrorEvent, isSuccess: $isSuccess)';
return 'DeviceSetupState(step: $step, firstName: $firstName, lastName: $lastName, bornAt: $bornAt, genrer: $genrer, relationType: $relationType, weight: $weight, height: $height, watchQr: $watchQr, watchCode: $watchCode, activationKey: $activationKey, isLoading: $isLoading, validationErrorKey: $validationErrorKey, apiErrorEvent: $apiErrorEvent, isSuccess: $isSuccess)';
}
}
/// @nodoc
abstract mixin class _$AddKidFlowStateCopyWith<$Res> implements $LegacyDeviceSetupViewStateCopyWith<$Res> {
factory _$AddKidFlowStateCopyWith(_AddKidFlowState value, $Res Function(_AddKidFlowState) _then) = __$AddKidFlowStateCopyWithImpl;
abstract mixin class _$DeviceSetupStateCopyWith<$Res> implements $DeviceSetupStateCopyWith<$Res> {
factory _$DeviceSetupStateCopyWith(_DeviceSetupState value, $Res Function(_DeviceSetupState) _then) = __$DeviceSetupStateCopyWithImpl;
@override @useResult
$Res call({
LegacyAddKidStep step, String firstName, String lastName, DateTime? bornAt, String genrer, String relationType, String weight, String height, String watchQr, String watchCode, String activationKey, bool isLoading, String validationErrorKey, LegacyDeviceSetupErrorEvent? apiErrorEvent, bool isSuccess
@@ -277,17 +277,17 @@ $Res call({
}
/// @nodoc
class __$AddKidFlowStateCopyWithImpl<$Res>
implements _$AddKidFlowStateCopyWith<$Res> {
__$AddKidFlowStateCopyWithImpl(this._self, this._then);
class __$DeviceSetupStateCopyWithImpl<$Res>
implements _$DeviceSetupStateCopyWith<$Res> {
__$DeviceSetupStateCopyWithImpl(this._self, this._then);
final _AddKidFlowState _self;
final $Res Function(_AddKidFlowState) _then;
final _DeviceSetupState _self;
final $Res Function(_DeviceSetupState) _then;
/// Create a copy of LegacyDeviceSetupViewState
/// Create a copy of DeviceSetupState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? step = null,Object? firstName = null,Object? lastName = null,Object? bornAt = freezed,Object? genrer = null,Object? relationType = null,Object? weight = null,Object? height = null,Object? watchQr = null,Object? watchCode = null,Object? activationKey = null,Object? isLoading = null,Object? validationErrorKey = null,Object? apiErrorEvent = freezed,Object? isSuccess = null,}) {
return _then(_AddKidFlowState(
return _then(_DeviceSetupState(
step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable
as LegacyAddKidStep,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String,lastName: null == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable

View File

@@ -0,0 +1,34 @@
import 'dart:async';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/qr_scanner_state.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'qr_scanner_controller.g.dart';
@riverpod
class QrScannerController extends _$QrScannerController {
Timer? _hideTimer;
@override
QrScannerState build() {
ref.onDispose(() => _hideTimer?.cancel());
return const QrScannerState();
}
bool markReturned() {
if (state.alreadyReturned) return false;
state = state.copyWith(alreadyReturned: true);
return true;
}
void showInvalid(String messageKey) {
_hideTimer?.cancel();
state = state.copyWith(
bannerVisible: true,
bannerMessageKey: messageKey,
);
_hideTimer = Timer(const Duration(seconds: 2), () {
state = state.copyWith(bannerVisible: false);
});
}
}

View File

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

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:sf_localizations/sf_localizations.dart';
part 'qr_scanner_state.freezed.dart';
@freezed
abstract class QrScannerState with _$QrScannerState {
const factory QrScannerState({
@Default(false) bool alreadyReturned,
@Default(false) bool bannerVisible,
@Default(I18n.deviceSetupQrNonNumeric) String bannerMessageKey,
}) = _QrScannerState;
}

View File

@@ -0,0 +1,277 @@
// 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 'qr_scanner_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$QrScannerState {
bool get alreadyReturned; bool get bannerVisible; String get bannerMessageKey;
/// Create a copy of QrScannerState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$QrScannerStateCopyWith<QrScannerState> get copyWith => _$QrScannerStateCopyWithImpl<QrScannerState>(this as QrScannerState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is QrScannerState&&(identical(other.alreadyReturned, alreadyReturned) || other.alreadyReturned == alreadyReturned)&&(identical(other.bannerVisible, bannerVisible) || other.bannerVisible == bannerVisible)&&(identical(other.bannerMessageKey, bannerMessageKey) || other.bannerMessageKey == bannerMessageKey));
}
@override
int get hashCode => Object.hash(runtimeType,alreadyReturned,bannerVisible,bannerMessageKey);
@override
String toString() {
return 'QrScannerState(alreadyReturned: $alreadyReturned, bannerVisible: $bannerVisible, bannerMessageKey: $bannerMessageKey)';
}
}
/// @nodoc
abstract mixin class $QrScannerStateCopyWith<$Res> {
factory $QrScannerStateCopyWith(QrScannerState value, $Res Function(QrScannerState) _then) = _$QrScannerStateCopyWithImpl;
@useResult
$Res call({
bool alreadyReturned, bool bannerVisible, String bannerMessageKey
});
}
/// @nodoc
class _$QrScannerStateCopyWithImpl<$Res>
implements $QrScannerStateCopyWith<$Res> {
_$QrScannerStateCopyWithImpl(this._self, this._then);
final QrScannerState _self;
final $Res Function(QrScannerState) _then;
/// Create a copy of QrScannerState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? alreadyReturned = null,Object? bannerVisible = null,Object? bannerMessageKey = null,}) {
return _then(_self.copyWith(
alreadyReturned: null == alreadyReturned ? _self.alreadyReturned : alreadyReturned // ignore: cast_nullable_to_non_nullable
as bool,bannerVisible: null == bannerVisible ? _self.bannerVisible : bannerVisible // ignore: cast_nullable_to_non_nullable
as bool,bannerMessageKey: null == bannerMessageKey ? _self.bannerMessageKey : bannerMessageKey // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [QrScannerState].
extension QrScannerStatePatterns on QrScannerState {
/// 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( _QrScannerState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _QrScannerState() 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( _QrScannerState value) $default,){
final _that = this;
switch (_that) {
case _QrScannerState():
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( _QrScannerState value)? $default,){
final _that = this;
switch (_that) {
case _QrScannerState() 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 alreadyReturned, bool bannerVisible, String bannerMessageKey)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _QrScannerState() when $default != null:
return $default(_that.alreadyReturned,_that.bannerVisible,_that.bannerMessageKey);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 alreadyReturned, bool bannerVisible, String bannerMessageKey) $default,) {final _that = this;
switch (_that) {
case _QrScannerState():
return $default(_that.alreadyReturned,_that.bannerVisible,_that.bannerMessageKey);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 alreadyReturned, bool bannerVisible, String bannerMessageKey)? $default,) {final _that = this;
switch (_that) {
case _QrScannerState() when $default != null:
return $default(_that.alreadyReturned,_that.bannerVisible,_that.bannerMessageKey);case _:
return null;
}
}
}
/// @nodoc
class _QrScannerState implements QrScannerState {
const _QrScannerState({this.alreadyReturned = false, this.bannerVisible = false, this.bannerMessageKey = I18n.deviceSetupQrNonNumeric});
@override@JsonKey() final bool alreadyReturned;
@override@JsonKey() final bool bannerVisible;
@override@JsonKey() final String bannerMessageKey;
/// Create a copy of QrScannerState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$QrScannerStateCopyWith<_QrScannerState> get copyWith => __$QrScannerStateCopyWithImpl<_QrScannerState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _QrScannerState&&(identical(other.alreadyReturned, alreadyReturned) || other.alreadyReturned == alreadyReturned)&&(identical(other.bannerVisible, bannerVisible) || other.bannerVisible == bannerVisible)&&(identical(other.bannerMessageKey, bannerMessageKey) || other.bannerMessageKey == bannerMessageKey));
}
@override
int get hashCode => Object.hash(runtimeType,alreadyReturned,bannerVisible,bannerMessageKey);
@override
String toString() {
return 'QrScannerState(alreadyReturned: $alreadyReturned, bannerVisible: $bannerVisible, bannerMessageKey: $bannerMessageKey)';
}
}
/// @nodoc
abstract mixin class _$QrScannerStateCopyWith<$Res> implements $QrScannerStateCopyWith<$Res> {
factory _$QrScannerStateCopyWith(_QrScannerState value, $Res Function(_QrScannerState) _then) = __$QrScannerStateCopyWithImpl;
@override @useResult
$Res call({
bool alreadyReturned, bool bannerVisible, String bannerMessageKey
});
}
/// @nodoc
class __$QrScannerStateCopyWithImpl<$Res>
implements _$QrScannerStateCopyWith<$Res> {
__$QrScannerStateCopyWithImpl(this._self, this._then);
final _QrScannerState _self;
final $Res Function(_QrScannerState) _then;
/// Create a copy of QrScannerState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? alreadyReturned = null,Object? bannerVisible = null,Object? bannerMessageKey = null,}) {
return _then(_QrScannerState(
alreadyReturned: null == alreadyReturned ? _self.alreadyReturned : alreadyReturned // ignore: cast_nullable_to_non_nullable
as bool,bannerVisible: null == bannerVisible ? _self.bannerVisible : bannerVisible // ignore: cast_nullable_to_non_nullable
as bool,bannerMessageKey: null == bannerMessageKey ? _self.bannerMessageKey : bannerMessageKey // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

View File

@@ -1,48 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/qr_scanner_controller.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:sf_localizations/sf_localizations.dart';
class LegacyQrScannerScreen extends StatefulWidget {
class LegacyQrScannerScreen extends ConsumerStatefulWidget {
const LegacyQrScannerScreen({super.key});
@override
State<LegacyQrScannerScreen> createState() => _LegacyQrScannerScreenState();
ConsumerState<LegacyQrScannerScreen> createState() =>
_LegacyQrScannerScreenState();
}
class _LegacyQrScannerScreenState extends State<LegacyQrScannerScreen> {
late final MobileScannerController _controller;
class _LegacyQrScannerScreenState extends ConsumerState<LegacyQrScannerScreen>
with SingleTickerProviderStateMixin {
static final _numericPattern = RegExp(r'^\d+$');
bool _alreadyReturned = false;
late final MobileScannerController _controller;
late final AnimationController _scanLine;
@override
void initState() {
super.initState();
_controller = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates,
formats: const [BarcodeFormat.qrCode],
detectionSpeed: DetectionSpeed.normal,
);
_scanLine = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1800),
)..repeat(reverse: true);
}
@override
void dispose() {
_scanLine.dispose();
_controller.dispose();
super.dispose();
}
void _returnResult(String value) {
if (_alreadyReturned) return;
_alreadyReturned = true;
final notifier = ref.read(qrScannerControllerProvider.notifier);
if (!notifier.markReturned()) return;
Navigator.of(context).pop(value);
}
@override
Widget build(BuildContext context) {
final scannerState = ref.watch(qrScannerControllerProvider);
final scannerNotifier = ref.read(qrScannerControllerProvider.notifier);
return Scaffold(
backgroundColor: Colors.black,
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.black,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
title: Text(context.translate(I18n.deviceSetupScanQr)),
elevation: 0,
scrolledUnderElevation: 0,
surfaceTintColor: Colors.transparent,
actions: [
IconButton(
icon: const Icon(Icons.flash_on),
@@ -50,52 +65,142 @@ class _LegacyQrScannerScreenState extends State<LegacyQrScannerScreen> {
),
],
),
body: Stack(
children: [
MobileScanner(
controller: _controller,
onDetect: (capture) {
if (capture.barcodes.isEmpty) return;
body: LayoutBuilder(
builder: (context, constraints) {
const cutOutSize = 260.0;
final cutOutTop = (constraints.maxHeight - cutOutSize) / 2;
final bannerBottom = constraints.maxHeight - cutOutTop + 16;
final hintTop = cutOutTop + cutOutSize + 20;
final rawValue = capture.barcodes.first.rawValue;
if (rawValue == null || rawValue.isEmpty) return;
return Stack(
children: [
MobileScanner(
controller: _controller,
onDetect: (capture) {
if (capture.barcodes.isEmpty) return;
_returnResult(rawValue);
},
),
final barcode = capture.barcodes.first;
final rawValue = barcode.rawValue;
if (rawValue == null || rawValue.isEmpty) return;
const Positioned.fill(
child: LegacyQrScannerOverlay(
cutOutSize: 260,
borderRadius: 18,
borderWidth: 3,
),
),
if (barcode.format != BarcodeFormat.qrCode) {
scannerNotifier
.showInvalid(I18n.deviceSetupBarcodeNotSupported);
return;
}
Positioned(
left: 0,
right: 0,
bottom: 50,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(14),
),
child: Text(
context.translate(I18n.deviceSetupScanQrHint),
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 15),
if (!_numericPattern.hasMatch(rawValue)) {
scannerNotifier
.showInvalid(I18n.deviceSetupQrNonNumeric);
return;
}
_returnResult(rawValue);
},
),
Positioned.fill(
child: LegacyQrScannerOverlay(
cutOutSize: cutOutSize,
borderRadius: 18,
borderWidth: 3,
scanLine: _scanLine,
),
),
Positioned(
left: 20,
right: 20,
bottom: bannerBottom,
child: _InvalidQrBanner(
visible: scannerState.bannerVisible,
messageKey: scannerState.bannerMessageKey,
),
),
Positioned(
left: 20,
right: 20,
top: hintTop,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(14),
),
child: Text(
context.translate(I18n.deviceSetupScanQrHint),
textAlign: TextAlign.center,
style:
const TextStyle(color: Colors.white, fontSize: 15),
),
),
),
],
);
},
),
);
}
}
class _InvalidQrBanner extends StatelessWidget {
const _InvalidQrBanner({required this.visible, required this.messageKey});
final bool visible;
final String messageKey;
static const _bgColor = Color(0xFFE53935);
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: AnimatedSlide(
offset: visible ? Offset.zero : const Offset(0, -0.3),
duration: const Duration(milliseconds: 220),
curve: Curves.easeOut,
child: AnimatedOpacity(
opacity: visible ? 1 : 0,
duration: const Duration(milliseconds: 220),
curve: Curves.easeOut,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: _bgColor,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: _bgColor.withValues(alpha: 0.4),
blurRadius: 18,
spreadRadius: 1,
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline_rounded,
color: Colors.white,
size: 22,
),
const SizedBox(width: 10),
Flexible(
child: Text(
context.translate(messageKey),
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
height: 1.3,
),
),
),
],
),
),
],
),
),
);
}
@@ -107,20 +212,26 @@ class LegacyQrScannerOverlay extends StatelessWidget {
required this.cutOutSize,
required this.borderRadius,
required this.borderWidth,
required this.scanLine,
});
final double cutOutSize;
final double borderRadius;
final double borderWidth;
final Animation<double> scanLine;
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: CustomPaint(
painter: _LegacyQrScannerOverlayPainter(
cutOutSize: cutOutSize,
borderRadius: borderRadius,
borderWidth: borderWidth,
child: AnimatedBuilder(
animation: scanLine,
builder: (_, __) => CustomPaint(
painter: _LegacyQrScannerOverlayPainter(
cutOutSize: cutOutSize,
borderRadius: borderRadius,
borderWidth: borderWidth,
scanLineProgress: scanLine.value,
),
),
),
);
@@ -132,11 +243,15 @@ class _LegacyQrScannerOverlayPainter extends CustomPainter {
required this.cutOutSize,
required this.borderRadius,
required this.borderWidth,
required this.scanLineProgress,
});
final double cutOutSize;
final double borderRadius;
final double borderWidth;
final double scanLineProgress;
static const _scanLineColor = Color(0xFF00E676);
@override
void paint(Canvas canvas, Size size) {
@@ -175,8 +290,59 @@ class _LegacyQrScannerOverlayPainter extends CustomPainter {
..color = Colors.white;
canvas.drawRRect(cutOutRRect, borderPaint);
canvas.save();
canvas.clipRRect(cutOutRRect);
final lineInset = 12.0;
final lineTop = cutOutRect.top + lineInset;
final lineBottom = cutOutRect.bottom - lineInset;
final lineY = lineTop + (lineBottom - lineTop) * scanLineProgress;
final glowPaint = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
_scanLineColor.withValues(alpha: 0.0),
_scanLineColor.withValues(alpha: 0.35),
_scanLineColor.withValues(alpha: 0.0),
],
stops: const [0.0, 0.5, 1.0],
).createShader(
Rect.fromLTWH(
cutOutRect.left,
lineY - 24,
cutOutRect.width,
48,
),
);
canvas.drawRect(
Rect.fromLTWH(
cutOutRect.left,
lineY - 24,
cutOutRect.width,
48,
),
glowPaint,
);
final linePaint = Paint()
..color = _scanLineColor
..strokeWidth = 2
..style = PaintingStyle.stroke;
canvas.drawLine(
Offset(cutOutRect.left + 8, lineY),
Offset(cutOutRect.right - 8, lineY),
linePaint,
);
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
bool shouldRepaint(covariant _LegacyQrScannerOverlayPainter oldDelegate) =>
oldDelegate.scanLineProgress != scanLineProgress;
}

View File

@@ -1,369 +0,0 @@
import 'dart:async';
import 'package:legacy_auth/src/core/domain/repositories/device_setup_repository.dart';
import 'package:legacy_auth/src/core/providers/device_setup_repository_provider.dart';
import 'package:legacy_auth/src/core/utils/date_format_utils.dart';
import 'package:legacy_auth/src/core/utils/text_format_utils.dart';
import 'package:legacy_auth/src/features/device_setup/domain/entities/legacy_device_setup_error_event.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/state/device_setup_view_state.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/enums/add_kid_step.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'package:utils/utils.dart';
final legacyDeviceSetupViewModelProvider =
NotifierProvider<LegacyDeviceSetupViewModel, LegacyDeviceSetupViewState>(
LegacyDeviceSetupViewModel.new,
);
class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
late final LegacyDeviceSetupRepository _deviceSetupRepository;
late final SfTrackingRepository _tracking;
late final TextEditingController firstNameController;
late final TextEditingController lastNameController;
late final TextEditingController bornAtController;
late final TextEditingController weightController;
late final TextEditingController heightController;
late final TextEditingController watchCodeController;
late final TextEditingController activationKeyController;
DateTime? _currentStepEnteredAt;
@override
LegacyDeviceSetupViewState build() {
final initial = const LegacyDeviceSetupViewState();
_initControllers(initial);
_addListeners();
_tracking = ref.read(sfTrackingProvider);
_currentStepEnteredAt = DateTime.now();
unawaited(_tracking.legacyDeviceSetupStarted());
ref.onDispose(disposeControllers);
return initial;
}
void _completeCurrentStep(String stepName) {
final enteredAt = _currentStepEnteredAt;
if (enteredAt == null) return;
final duration = DateTime.now().difference(enteredAt).inSeconds;
unawaited(
_tracking.legacyDeviceSetupStepCompleted(
step: stepName,
durationSeconds: duration,
),
);
_currentStepEnteredAt = DateTime.now();
}
void _initControllers(LegacyDeviceSetupViewState s) {
_deviceSetupRepository = ref.read(legacyDeviceSetupRepositoryProvider);
firstNameController = TextEditingController(text: s.firstName);
lastNameController = TextEditingController(text: s.lastName);
bornAtController = TextEditingController(
text: s.bornAt == null ? '' : formatDateDMY(s.bornAt!),
);
weightController = TextEditingController(text: s.weight);
heightController = TextEditingController(text: s.height);
watchCodeController = TextEditingController(text: s.watchCode);
activationKeyController = TextEditingController(text: s.activationKey);
}
void _addListeners() {
firstNameController.addListener(_onFirstNameChanged);
lastNameController.addListener(_onLastNameChanged);
bornAtController.addListener(_onBornAtTextChanged);
weightController.addListener(_onWeightChanged);
heightController.addListener(_onHeightChanged);
watchCodeController.addListener(_onWatchCodeChanged);
activationKeyController.addListener(_onActivationKeyChanged);
}
Future<void> next() async {
switch (state.step) {
case LegacyAddKidStep.intro:
_completeCurrentStep('intro');
state = state.copyWith(step: LegacyAddKidStep.linkInfo);
return;
case LegacyAddKidStep.linkInfo:
_completeCurrentStep('link_info');
state = state.copyWith(step: LegacyAddKidStep.scanWatch);
return;
case LegacyAddKidStep.scanWatch:
final identificator = state.watchCode.isNotEmpty
? state.watchCode
: state.watchQr;
if (identificator.isEmpty) {
state = state.copyWith(validationErrorKey: '');
state = state.copyWith(validationErrorKey: I18n.errorScanWatchRequired);
return;
}
if (state.watchCode.isNotEmpty && state.watchQr.isEmpty) {
unawaited(_tracking.legacyDeviceSetupManualCodeEntered());
}
await _generateActivationKey(identificator);
return;
case LegacyAddKidStep.profile:
return;
}
}
Future<void> _generateActivationKey(String identificator) async {
state = state.copyWith(
isLoading: true,
validationErrorKey: '',
apiErrorEvent: null,
);
try {
await _deviceSetupRepository.generateActivationKey(
identificator: identificator,
);
if (!ref.mounted) return;
_completeCurrentStep('scan_watch');
state = state.copyWith(isLoading: false, step: LegacyAddKidStep.profile);
} catch (e) {
if (!ref.mounted) return;
unawaited(
_tracking.legacyDeviceSetupFailed(
atStep: 'scan_watch',
reason: e.toString(),
),
);
state = state.copyWith(
isLoading: false,
apiErrorEvent: mapActivationKeyError(e),
);
}
}
void back() {
switch (state.step) {
case LegacyAddKidStep.intro:
return;
case LegacyAddKidStep.linkInfo:
unawaited(_tracking.legacyDeviceSetupCancelled('link_info'));
state = state.copyWith(step: LegacyAddKidStep.intro, validationErrorKey: '');
return;
case LegacyAddKidStep.scanWatch:
unawaited(_tracking.legacyDeviceSetupCancelled('scan_watch'));
state = state.copyWith(step: LegacyAddKidStep.linkInfo);
return;
case LegacyAddKidStep.profile:
unawaited(_tracking.legacyDeviceSetupCancelled('profile'));
state = state.copyWith(step: LegacyAddKidStep.scanWatch);
return;
}
}
void onWatchQrScanned(String qr) {
unawaited(_tracking.legacyDeviceSetupQrScanned());
_completeCurrentStep('scan_watch');
state = state.copyWith(watchQr: qr, step: LegacyAddKidStep.profile);
}
void setBornAt(DateTime date) {
bornAtController.text = formatDateDMY(date);
state = state.copyWith(bornAt: date);
}
Future<bool> createDevice() async {
final name = '${state.firstName.trim()} ${state.lastName.trim()}'
.trim()
.toUpperCase();
final birth = state.bornAt!;
final bornAt = DateTime.utc(
birth.year,
birth.month,
birth.day,
).millisecondsSinceEpoch;
final weight = int.parse(state.weight.trim());
final heightCm = double.parse(state.height.trim());
final stepLength = (heightCm * 0.40).round();
final genrer = state.genrer.trim();
final relationType = state.relationType.trim();
final activationKey = state.activationKey.trim();
state = state.copyWith(
isLoading: true,
apiErrorEvent: null,
validationErrorKey: '',
);
try {
await _deviceSetupRepository.createDevice(
name: name,
genrer: genrer,
weight: weight,
stepLength: stepLength,
bornAt: bornAt,
relationType: relationType,
activationKey: activationKey,
);
if (!ref.mounted) return false;
// We don't have the new DeviceEntity locally (the create endpoint
// returns void), so re-read from the backend.
await ref.read(legacyDevicesProvider.notifier).refresh();
unawaited(
_tracking.legacyDeviceSetupCompleted(
childGender: genrer,
relationType: relationType,
childAgeYears: yearsBetween(birth, DateTime.now()),
),
);
state = state.copyWith(isLoading: false, isSuccess: true);
return true;
} catch (e) {
if (!ref.mounted) return false;
unawaited(
_tracking.legacyDeviceSetupFailed(
atStep: 'profile',
reason: e.toString(),
),
);
state = state.copyWith(
isLoading: false,
apiErrorEvent: mapActivateDeviceError(e),
);
return false;
}
}
bool validateProfile() {
final isInvalid =
state.firstName.trim().isEmpty ||
state.lastName.trim().isEmpty ||
state.bornAt == null ||
state.genrer.trim().isEmpty ||
state.relationType.trim().isEmpty ||
state.weight.trim().isEmpty ||
int.tryParse(state.weight.trim()) == null ||
state.height.trim().isEmpty ||
double.tryParse(state.height.trim()) == null ||
state.activationKey.trim().isEmpty;
if (isInvalid) {
state = state.copyWith(validationErrorKey: '');
state = state.copyWith(validationErrorKey: I18n.errorAllFieldsRequired);
return false;
}
return true;
}
void _onFirstNameChanged() {
toCapitalizedController(firstNameController);
final text = firstNameController.text;
if (text == state.firstName) return;
state = state.copyWith(firstName: text, validationErrorKey: '');
}
void _onLastNameChanged() {
toCapitalizedController(lastNameController);
final text = lastNameController.text;
if (text == state.lastName) return;
state = state.copyWith(lastName: text, validationErrorKey: '');
}
void _onBornAtTextChanged() {
final text = bornAtController.text;
final parsed = tryParseDMY(text);
if (text.trim().isEmpty) {
if (state.bornAt != null) {
state = state.copyWith(bornAt: null, validationErrorKey: '');
}
return;
}
if (parsed != null && parsed != state.bornAt) {
state = state.copyWith(bornAt: parsed, validationErrorKey: '');
}
}
void _onWeightChanged() {
final text = weightController.text;
if (text == state.weight) return;
state = state.copyWith(weight: text, validationErrorKey: '');
}
void _onHeightChanged() {
final text = heightController.text;
if (text == state.height) return;
state = state.copyWith(height: text, validationErrorKey: '');
}
void _onWatchCodeChanged() {
final text = watchCodeController.text;
if (text == state.watchCode) return;
state = state.copyWith(watchCode: text, validationErrorKey: '');
}
void _onActivationKeyChanged() {
toUpperCaseController(activationKeyController);
final text = activationKeyController.text;
if (text == state.activationKey) return;
state = state.copyWith(activationKey: text, validationErrorKey: '');
}
void setValidationError(String key) {
state = state.copyWith(validationErrorKey: key);
}
void clearApiError() {
if (state.apiErrorEvent != null) state = state.copyWith(apiErrorEvent: null);
}
void clearValidationError() {
if (state.validationErrorKey.isNotEmpty) {
state = state.copyWith(validationErrorKey: '');
}
}
void onGenrerChanged(String? value) {
final v = value ?? '';
if (v == state.genrer) return;
state = state.copyWith(genrer: v, validationErrorKey: '');
}
void onRelationTypeChanged(String? value) {
final v = value ?? '';
if (v == state.relationType) return;
state = state.copyWith(relationType: v, validationErrorKey: '');
}
void resetForNewKid() {
unawaited(_tracking.legacyDeviceSetupResetForNewKid());
_currentStepEnteredAt = DateTime.now();
firstNameController.clear();
lastNameController.clear();
bornAtController.clear();
weightController.clear();
heightController.clear();
watchCodeController.clear();
activationKeyController.clear();
state = const LegacyDeviceSetupViewState();
}
void disposeControllers() {
firstNameController.dispose();
lastNameController.dispose();
bornAtController.dispose();
weightController.dispose();
heightController.dispose();
watchCodeController.dispose();
activationKeyController.dispose();
}
}

View File

@@ -1,26 +1,32 @@
import 'package:legacy_auth/src/features/device_setup/presentation/state/device_setup_view_state.dart';
import 'package:flutter/material.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/enums/add_kid_step.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_form_controllers.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/steps/intro_step.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/steps/link_info_step.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/steps/profile_step.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/steps/scan_watch_step.dart';
import 'package:flutter/material.dart';
class LegacyStepBody extends StatelessWidget {
const LegacyStepBody({super.key, required this.state});
final LegacyDeviceSetupViewState state;
const LegacyStepBody({
super.key,
required this.step,
required this.controllers,
});
final LegacyAddKidStep step;
final DeviceSetupFormControllers controllers;
@override
Widget build(BuildContext context) {
switch (state.step) {
switch (step) {
case LegacyAddKidStep.intro:
return LegacyIntroStepScreen();
return const LegacyIntroStepScreen();
case LegacyAddKidStep.linkInfo:
return LegacyLinkInfoStepScreen();
return const LegacyLinkInfoStepScreen();
case LegacyAddKidStep.scanWatch:
return LegacyScanWatchStepScreen();
return LegacyScanWatchStepScreen(controllers: controllers);
case LegacyAddKidStep.profile:
return LegacyProfileStepScreen();
return LegacyProfileStepScreen(controllers: controllers);
}
}
}

View File

@@ -1,7 +1,8 @@
import 'package:legacy_auth/src/features/device_setup/presentation/state/device_setup_view_model.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_controller.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_form_controllers.dart';
import 'package:sf_localizations/sf_localizations.dart';
Future<void> _pickBornAt(
@@ -26,7 +27,9 @@ Future<void> _pickBornAt(
}
class LegacyProfileStepScreen extends ConsumerWidget {
const LegacyProfileStepScreen({super.key});
final DeviceSetupFormControllers controllers;
const LegacyProfileStepScreen({super.key, required this.controllers});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -42,8 +45,8 @@ class LegacyProfileStepScreen extends ConsumerWidget {
'OTHER': context.translate(I18n.relationshipOther),
};
final state = ref.watch(legacyDeviceSetupViewModelProvider);
final vm = ref.read(legacyDeviceSetupViewModelProvider.notifier);
final state = ref.watch(deviceSetupControllerProvider);
final notifier = ref.read(deviceSetupControllerProvider.notifier);
return SingleChildScrollView(
child: Column(
@@ -78,31 +81,29 @@ class LegacyProfileStepScreen extends ConsumerWidget {
CustomTextField(
label: context.translate(I18n.firstNameLabel),
hint: context.translate(I18n.firstNameHint),
controller: vm.firstNameController,
controller: controllers.firstName,
),
const SizedBox(height: 8),
CustomTextField(
label: context.translate(I18n.lastNameLabel),
hint: context.translate(I18n.lastNameHint),
controller: vm.lastNameController,
controller: controllers.lastName,
),
const SizedBox(height: 8),
GestureDetector(
onTap: () => _pickBornAt(context, state.bornAt, vm.setBornAt),
onTap: () =>
_pickBornAt(context, state.bornAt, notifier.setBornAt),
child: AbsorbPointer(
child: CustomTextField(
label: context.translate(I18n.birthDateLabel),
hint: context.translate(I18n.birthDateHint),
controller: vm.bornAtController,
controller: controllers.bornAt,
readOnly: true,
keyboardType: TextInputType.none,
),
),
),
const SizedBox(height: 8),
CustomDropdown(
items: genrerItems.values
.map(Text.new)
@@ -111,11 +112,9 @@ class LegacyProfileStepScreen extends ConsumerWidget {
value: state.genrer.isEmpty ? null : state.genrer,
label: context.translate(I18n.genderLabel),
hint: context.translate(I18n.genderHint),
onChanged: (v) => vm.onGenrerChanged(v as String?),
onChanged: (v) => notifier.onGenrerChanged(v as String?),
),
const SizedBox(height: 8),
CustomDropdown(
items: relationshipItems.values
.map(Text.new)
@@ -124,35 +123,29 @@ class LegacyProfileStepScreen extends ConsumerWidget {
value: state.relationType.isEmpty ? null : state.relationType,
label: context.translate(I18n.relationshipLabel),
hint: context.translate(I18n.relationshipHint),
onChanged: (v) => vm.onRelationTypeChanged(v as String?),
onChanged: (v) =>
notifier.onRelationTypeChanged(v as String?),
),
const SizedBox(height: 8),
CustomTextField(
label: context.translate(I18n.deviceSetupWeightLabel),
hint: context.translate(I18n.deviceSetupWeightHint),
controller: vm.weightController,
controller: controllers.weight,
keyboardType: TextInputType.number,
),
const SizedBox(height: 8),
CustomTextField(
label: context.translate(I18n.deviceSetupHeightLabel),
hint: context.translate(I18n.deviceSetupHeightHint),
controller: vm.heightController,
controller: controllers.height,
keyboardType: TextInputType.number,
),
const SizedBox(height: 8),
CustomTextField(
label: context.translate(I18n.activationKeyLabel),
hint: 'XXXXXXXX',
controller: vm.activationKeyController,
controller: controllers.activationKey,
),
const SizedBox(height: 8),
],
),

View File

@@ -1,19 +1,20 @@
import 'package:legacy_auth/src/features/device_setup/presentation/qr_scanner_screen.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/state/device_setup_view_model.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_auth/src/features/device_setup/presentation/providers/device_setup_controller.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_form_controllers.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/qr_scanner_screen.dart';
import 'package:sf_localizations/sf_localizations.dart';
import '../widgets/activation_code_dialog.dart';
class LegacyScanWatchStepScreen extends ConsumerWidget {
const LegacyScanWatchStepScreen({super.key});
final DeviceSetupFormControllers controllers;
const LegacyScanWatchStepScreen({super.key, required this.controllers});
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(legacyDeviceSetupViewModelProvider.notifier);
final notifier = ref.read(deviceSetupControllerProvider.notifier);
return SingleChildScrollView(
child: Column(
@@ -21,7 +22,6 @@ class LegacyScanWatchStepScreen extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 65),
child: Text(
@@ -34,9 +34,7 @@ class LegacyScanWatchStepScreen extends ConsumerWidget {
textAlign: TextAlign.center,
),
),
const SizedBox(height: 28),
InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () async {
@@ -47,19 +45,22 @@ class LegacyScanWatchStepScreen extends ConsumerWidget {
);
if (result == null || result.isEmpty) return;
if (!context.mounted) return;
vm.onWatchQrScanned(result);
showActivationCodeDialog(context);
controllers.watchCode.text = result;
notifier.onWatchQrScanned(result);
},
child: Container(
width: 170,
height: 170,
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.onSurfaceVariant, width: 1),
border: Border.all(
color: Theme.of(context).colorScheme.onSurfaceVariant,
width: 1,
),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: SvgPicture.asset(
"assets/shared/images/qr.svg",
'assets/shared/images/qr.svg',
width: 90,
height: 90,
fit: BoxFit.contain,
@@ -67,9 +68,7 @@ class LegacyScanWatchStepScreen extends ConsumerWidget {
),
),
),
const SizedBox(height: 22),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
@@ -84,8 +83,8 @@ class LegacyScanWatchStepScreen extends ConsumerWidget {
children: [
Expanded(
child: CustomTextField(
hint: "XXXXXXXXXX",
controller: vm.watchCodeController,
hint: 'XXXXXXXXXX',
controller: controllers.watchCode,
),
),
const SizedBox(width: 12),
@@ -94,15 +93,13 @@ class LegacyScanWatchStepScreen extends ConsumerWidget {
],
),
),
const SizedBox(height: 10),
Column(
children: [
Text(
context.translate(I18n.legacyDeviceSetupLinkTroubleshootTitle),
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
style: const TextStyle(fontSize: 18),
),
CustomTextButton(
onPressed: () {},

View File

@@ -1,20 +1,26 @@
import 'package:legacy_auth/src/features/device_setup/presentation/state/device_setup_view_model.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/widgets/flow_footer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_controller.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/providers/device_setup_form_controllers.dart';
import 'package:legacy_auth/src/features/device_setup/presentation/widgets/flow_footer.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
class LegacySuccessScreen extends ConsumerWidget {
final NavigationContract navigationContract;
final DeviceSetupFormControllers formControllers;
const LegacySuccessScreen({super.key, required this.navigationContract});
const LegacySuccessScreen({
super.key,
required this.navigationContract,
required this.formControllers,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(legacyDeviceSetupViewModelProvider.notifier);
final state = ref.watch(legacyDeviceSetupViewModelProvider);
final notifier = ref.read(deviceSetupControllerProvider.notifier);
final state = ref.watch(deviceSetupControllerProvider);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
@@ -64,7 +70,14 @@ class LegacySuccessScreen extends ConsumerWidget {
},
secondaryText: context.translate(I18n.deviceSetupAddAnotherKid),
onSecondary: () {
vm.resetForNewKid();
notifier.resetForNewKid();
formControllers.firstName.clear();
formControllers.lastName.clear();
formControllers.bornAt.clear();
formControllers.weight.clear();
formControllers.height.clear();
formControllers.watchCode.clear();
formControllers.activationKey.clear();
Navigator.of(context).pop();
},
),

View File

@@ -211,6 +211,8 @@
"deviceSetupPaymentSuccess": "Zahlung erfolgreich abgeschlossen!",
"deviceSetupScanQr": "QR scannen",
"deviceSetupScanQrHint": "Richte den QR-Code innerhalb des Rahmens aus",
"deviceSetupQrNonNumeric": "Dieser QR ist ungültig. Scanne den Gerätecode.",
"deviceSetupBarcodeNotSupported": "Nur der QR-Code des Geräts wird unterstützt.",
"errorScanStrapRequired": "Scanne das Armband oder gib den Code ein, um fortzufahren",
"errorScanWatchRequired": "Scanne die Uhr oder gib den Code ein, um fortzufahren",
"errorAllFieldsRequired": "Bitte fülle alle Felder aus",

View File

@@ -211,6 +211,8 @@
"deviceSetupPaymentSuccess": "Payment completed successfully!",
"deviceSetupScanQr": "Scan QR",
"deviceSetupScanQrHint": "Center the QR inside the frame",
"deviceSetupQrNonNumeric": "This QR is not valid. Scan the device code.",
"deviceSetupBarcodeNotSupported": "Only the device QR is supported.",
"errorScanStrapRequired": "Scan the band or enter the code to continue",
"errorScanWatchRequired": "Scan the watch or enter the code to continue",
"errorAllFieldsRequired": "Please fill in all fields",

View File

@@ -211,6 +211,8 @@
"deviceSetupPaymentSuccess": "¡Pago realizado con éxito!",
"deviceSetupScanQr": "Escanear QR",
"deviceSetupScanQrHint": "Centra el QR dentro del recuadro",
"deviceSetupQrNonNumeric": "Este QR no es válido. Escanea el código del dispositivo.",
"deviceSetupBarcodeNotSupported": "Solo se admite el QR del dispositivo.",
"errorScanStrapRequired": "Escanea la correa o introduce el código para continuar",
"errorScanWatchRequired": "Escanea el reloj o introduce el código para continuar",
"errorAllFieldsRequired": "Completa todos los campos",

View File

@@ -211,6 +211,8 @@
"deviceSetupPaymentSuccess": "Paiement effectué avec succès !",
"deviceSetupScanQr": "Scanner le QR",
"deviceSetupScanQrHint": "Place le QR au centre du cadre",
"deviceSetupQrNonNumeric": "Ce QR n'est pas valide. Scanne le code de l'appareil.",
"deviceSetupBarcodeNotSupported": "Seul le QR de l'appareil est pris en charge.",
"errorScanStrapRequired": "Scannez le bracelet ou saisissez le code pour continuer",
"errorScanWatchRequired": "Scannez la montre ou saisissez le code pour continuer",
"errorAllFieldsRequired": "Veuillez remplir tous les champs",

View File

@@ -211,6 +211,8 @@
"deviceSetupPaymentSuccess": "Pagamento completato con successo!",
"deviceSetupScanQr": "Scansiona QR",
"deviceSetupScanQrHint": "Centra il QR all'interno del riquadro",
"deviceSetupQrNonNumeric": "Questo QR non è valido. Scansiona il codice del dispositivo.",
"deviceSetupBarcodeNotSupported": "È supportato solo il QR del dispositivo.",
"errorScanStrapRequired": "Scansiona il cinturino o inserisci il codice per continuare",
"errorScanWatchRequired": "Scansiona l'orologio o inserisci il codice per continuare",
"errorAllFieldsRequired": "Compila tutti i campi",

View File

@@ -211,6 +211,8 @@
"deviceSetupPaymentSuccess": "Pagamento realizado com sucesso!",
"deviceSetupScanQr": "Digitalizar QR",
"deviceSetupScanQrHint": "Centraliza o QR dentro da moldura",
"deviceSetupQrNonNumeric": "Este QR não é válido. Escaneia o código do dispositivo.",
"deviceSetupBarcodeNotSupported": "Apenas o QR do dispositivo é suportado.",
"errorScanStrapRequired": "Digitaliza a pulseira ou introduz o código para continuar",
"errorScanWatchRequired": "Digitaliza o relógio ou introduz o código para continuar",
"errorAllFieldsRequired": "Preenche todos os campos",

View File

@@ -305,6 +305,8 @@ class I18n {
static const String deviceSetupLinkTroubleshootTitle = 'deviceSetupLinkTroubleshootTitle';
static const String deviceSetupPaymentCancelled = 'deviceSetupPaymentCancelled';
static const String deviceSetupPaymentSuccess = 'deviceSetupPaymentSuccess';
static const String deviceSetupQrNonNumeric = 'deviceSetupQrNonNumeric';
static const String deviceSetupBarcodeNotSupported = 'deviceSetupBarcodeNotSupported';
static const String deviceSetupScanQr = 'deviceSetupScanQr';
static const String deviceSetupScanQrHint = 'deviceSetupScanQrHint';
static const String deviceSetupSkipAndConfigureLater = 'deviceSetupSkipAndConfigureLater';