refactor(legacy_auth): migrate device_setup to Riverpod + polish QR scanner UX
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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: () {},
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user