feat(tracking): expand legacy module analytics coverage
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:account/src/core/domain/repositories/users_repository.dart';
|
||||
import 'package:account/src/core/providers/users_repository_provider.dart';
|
||||
import 'package:account/src/features/app_users/presentation/state/app_users_view_state.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
final appUsersViewModelProvider =
|
||||
NotifierProvider.autoDispose<AppUsersViewModel, AppUsersViewState>(
|
||||
@@ -11,10 +14,12 @@ final appUsersViewModelProvider =
|
||||
|
||||
class AppUsersViewModel extends Notifier<AppUsersViewState> {
|
||||
late final UsersRepository _usersRepository;
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
@override
|
||||
AppUsersViewState build() {
|
||||
_usersRepository = ref.read(usersRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
|
||||
_init();
|
||||
|
||||
@@ -42,6 +47,7 @@ class AppUsersViewModel extends Notifier<AppUsersViewState> {
|
||||
}
|
||||
|
||||
void deleteUser() {
|
||||
unawaited(_tracking.legacyAccountAppUserDeleteTriggered());
|
||||
_usersRepository.deleteUser(userId: state.loggedUser!.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ class LinkedDevicesViewModel extends Notifier<LinkedDevicesViewState> {
|
||||
|
||||
try {
|
||||
_devicesRepository.updateDevice(request: _toRequest(device));
|
||||
unawaited(_tracking.legacyAccountLinkedDeviceRenamed());
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
|
||||
@@ -130,6 +130,10 @@ class ControlPanelViewModel extends Notifier<ControlPanelViewState> {
|
||||
|
||||
_selectedDeviceNotifier.setSelectedDevice(device);
|
||||
|
||||
unawaited(_tracking.legacyControlPanelDeviceSelected());
|
||||
unawaited(
|
||||
_tracking.legacyControlPanelDeviceSelected(
|
||||
totalDevices: state.devices.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,12 @@ class ContactViewModel extends Notifier<ContactViewState> {
|
||||
'mailto:$receiver?from=$sender&subject=$subject&body=$body',
|
||||
);
|
||||
|
||||
unawaited(_tracking.legacySupportContactInitiated('email'));
|
||||
unawaited(
|
||||
_tracking.legacySupportContactInitiated(
|
||||
channel: 'email',
|
||||
country: country,
|
||||
),
|
||||
);
|
||||
|
||||
if (!await launchUrl(url)) {
|
||||
throw Exception('Could not launch $url');
|
||||
|
||||
@@ -36,6 +36,11 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
Future<void> selectTimeRange(TimeRange range) async {
|
||||
if (range == state.timeRange) return;
|
||||
state = state.copyWith(timeRange: range, isLoading: true);
|
||||
unawaited(
|
||||
_tracking.legacyDeviceActivityMeterTimeRangeChanged(
|
||||
_timeRangeName(range),
|
||||
),
|
||||
);
|
||||
await _loadFilteredData();
|
||||
}
|
||||
|
||||
@@ -46,9 +51,25 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
customEnd: end,
|
||||
isLoading: true,
|
||||
);
|
||||
unawaited(
|
||||
_tracking.legacyDeviceActivityMeterTimeRangeChanged('custom'),
|
||||
);
|
||||
await _loadFilteredData();
|
||||
}
|
||||
|
||||
String _timeRangeName(TimeRange range) {
|
||||
switch (range) {
|
||||
case TimeRange.today:
|
||||
return 'today';
|
||||
case TimeRange.sevenDays:
|
||||
return 'seven_days';
|
||||
case TimeRange.thirtyDays:
|
||||
return 'thirty_days';
|
||||
case TimeRange.custom:
|
||||
return 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadMoreHistory() async {
|
||||
if (state.isLoadingMore || !state.hasMoreHistory) return;
|
||||
final identificator = _identificator;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_shared/legacy_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
import '../../../../core/data/datasources/health_query_builder.dart';
|
||||
import '../../../../core/domain/entities/app_usage_time_entity.dart';
|
||||
@@ -15,12 +18,14 @@ final appsUseViewModelProvider =
|
||||
|
||||
class AppsUseViewModel extends Notifier<AppsUseViewState> {
|
||||
late final AppUsageTimesRepository _repository;
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
static const int _pageSize = 20;
|
||||
|
||||
@override
|
||||
AppsUseViewState build() {
|
||||
_repository = ref.read(appUsageTimesRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
Future.microtask(_init);
|
||||
return const AppsUseViewState();
|
||||
}
|
||||
@@ -31,6 +36,7 @@ class AppsUseViewModel extends Notifier<AppsUseViewState> {
|
||||
if (range == state.timeRange) return;
|
||||
state = state.copyWith(timeRange: range, isLoading: true);
|
||||
await _loadData();
|
||||
_fireTimeRangeEvent();
|
||||
}
|
||||
|
||||
Future<void> selectCustomRange(DateTime start, DateTime end) async {
|
||||
@@ -41,6 +47,33 @@ class AppsUseViewModel extends Notifier<AppsUseViewState> {
|
||||
isLoading: true,
|
||||
);
|
||||
await _loadData();
|
||||
_fireTimeRangeEvent();
|
||||
}
|
||||
|
||||
void _fireTimeRangeEvent() {
|
||||
final topAppName = state.topApps.isNotEmpty
|
||||
? state.topApps.first.name
|
||||
: '';
|
||||
unawaited(
|
||||
_tracking.legacyDeviceAppsUseTimeRangeChanged(
|
||||
range: _timeRangeName(state.timeRange),
|
||||
totalDurationSeconds: state.totalDuration,
|
||||
topAppName: topAppName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _timeRangeName(TimeRange range) {
|
||||
switch (range) {
|
||||
case TimeRange.today:
|
||||
return 'today';
|
||||
case TimeRange.sevenDays:
|
||||
return 'seven_days';
|
||||
case TimeRange.thirtyDays:
|
||||
return 'thirty_days';
|
||||
case TimeRange.custom:
|
||||
return 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_shared/legacy_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
import '../../data/call_history_datasource.dart';
|
||||
import '../../data/call_history_datasource_provider.dart';
|
||||
@@ -13,10 +16,12 @@ final callHistoryViewModelProvider =
|
||||
|
||||
class CallHistoryViewModel extends Notifier<CallHistoryViewState> {
|
||||
late final CallHistoryDatasource _datasource;
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
@override
|
||||
CallHistoryViewState build() {
|
||||
_datasource = ref.read(callHistoryDatasourceProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
Future.microtask(() => _load());
|
||||
return const CallHistoryViewState();
|
||||
}
|
||||
@@ -46,6 +51,12 @@ class CallHistoryViewModel extends Notifier<CallHistoryViewState> {
|
||||
}
|
||||
|
||||
void setFilter(CallFilter filter) {
|
||||
if (filter == state.filter) return;
|
||||
|
||||
unawaited(
|
||||
_tracking.legacyDeviceCallHistoryFilterChanged(_filterName(filter)),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
filter: filter,
|
||||
filteredCalls: _applyFilter(state.calls, filter),
|
||||
@@ -67,4 +78,17 @@ class CallHistoryViewModel extends Notifier<CallHistoryViewState> {
|
||||
return calls.where((c) => !c.isAccepted).toList();
|
||||
}
|
||||
}
|
||||
|
||||
String _filterName(CallFilter filter) {
|
||||
switch (filter) {
|
||||
case CallFilter.all:
|
||||
return 'all';
|
||||
case CallFilter.incoming:
|
||||
return 'incoming';
|
||||
case CallFilter.outgoing:
|
||||
return 'outgoing';
|
||||
case CallFilter.missed:
|
||||
return 'missed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,9 @@ class ContactsViewModel extends Notifier<ContactsViewState> {
|
||||
await _contactsRepository.createContact(request: request);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
unawaited(_tracking.legacyContactsAdded());
|
||||
unawaited(
|
||||
_tracking.legacyContactsAdded(totalCount: state.contacts.length + 1),
|
||||
);
|
||||
|
||||
await _reload();
|
||||
return true;
|
||||
@@ -150,7 +152,11 @@ class ContactsViewModel extends Notifier<ContactsViewState> {
|
||||
await _contactsRepository.deleteContact(contactId: contact.id);
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
unawaited(_tracking.legacyContactsDeleted());
|
||||
unawaited(
|
||||
_tracking.legacyContactsDeleted(
|
||||
totalCount: (state.contacts.length - 1).clamp(0, 999),
|
||||
),
|
||||
);
|
||||
|
||||
await _reload();
|
||||
return true;
|
||||
|
||||
@@ -70,6 +70,9 @@ class HealthViewModel extends Notifier<HealthViewState> {
|
||||
Future<void> selectTimeRange(TimeRange range) async {
|
||||
if (range == state.timeRange) return;
|
||||
state = state.copyWith(timeRange: range, isLoading: true, errorEvent: null);
|
||||
unawaited(
|
||||
_tracking.legacyDeviceHealthTimeRangeChanged(_timeRangeName(range)),
|
||||
);
|
||||
await _loadFilteredData();
|
||||
}
|
||||
|
||||
@@ -81,9 +84,23 @@ class HealthViewModel extends Notifier<HealthViewState> {
|
||||
isLoading: true,
|
||||
errorEvent: null,
|
||||
);
|
||||
unawaited(_tracking.legacyDeviceHealthTimeRangeChanged('custom'));
|
||||
await _loadFilteredData();
|
||||
}
|
||||
|
||||
String _timeRangeName(TimeRange range) {
|
||||
switch (range) {
|
||||
case TimeRange.today:
|
||||
return 'today';
|
||||
case TimeRange.sevenDays:
|
||||
return 'seven_days';
|
||||
case TimeRange.thirtyDays:
|
||||
return 'thirty_days';
|
||||
case TimeRange.custom:
|
||||
return 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadMoreHistory() async {
|
||||
if (state.isLoadingMore || !state.hasMoreHistory) return;
|
||||
final identificator = _identificator;
|
||||
|
||||
@@ -28,6 +28,8 @@ class LocateDeviceViewModel extends Notifier<LocateDeviceViewState> {
|
||||
Future<void> locateDevice() async {
|
||||
if (state.isLoading) return;
|
||||
|
||||
unawaited(_tracking.legacyDeviceLocateRequested());
|
||||
|
||||
try {
|
||||
state = state.copyWith(
|
||||
isLoading: true,
|
||||
@@ -41,10 +43,12 @@ class LocateDeviceViewModel extends Notifier<LocateDeviceViewState> {
|
||||
);
|
||||
await _commandsRepository.send(request: request);
|
||||
|
||||
unawaited(_tracking.legacyDeviceLocateRequested());
|
||||
unawaited(_tracking.legacyDeviceLocateSuccess());
|
||||
|
||||
state = state.copyWith(isLoading: false, isComplete: true);
|
||||
} catch (e) {
|
||||
unawaited(_tracking.legacyDeviceLocateFailure(e.toString()));
|
||||
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
isComplete: false,
|
||||
|
||||
@@ -81,16 +81,19 @@ class RemoteConnectionViewModel extends Notifier<RemoteConnectionViewState> {
|
||||
pictureIndex = state.pictures.length - 1;
|
||||
}
|
||||
|
||||
unawaited(_tracking.legacyDeviceRemoteConnectionPictureViewed('prev'));
|
||||
state = state.copyWith(pictureIndex: pictureIndex);
|
||||
}
|
||||
|
||||
void nextPicture() {
|
||||
int pictureIndex = (state.pictureIndex + 1) % state.pictures.length;
|
||||
|
||||
unawaited(_tracking.legacyDeviceRemoteConnectionPictureViewed('next'));
|
||||
state = state.copyWith(pictureIndex: pictureIndex);
|
||||
}
|
||||
|
||||
void setPictureIndex(int value) {
|
||||
unawaited(_tracking.legacyDeviceRemoteConnectionPictureViewed('direct'));
|
||||
state = state.copyWith(pictureIndex: value);
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,12 @@ class ScheduledActivitiesViewModel
|
||||
await _repository.createActivity(request: request);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
unawaited(_tracking.legacyDeviceScheduledActivityAdded());
|
||||
unawaited(
|
||||
_tracking.legacyDeviceScheduledActivityAdded(
|
||||
weekDay: weekDay,
|
||||
period: period,
|
||||
),
|
||||
);
|
||||
|
||||
await _reload();
|
||||
} catch (e) {
|
||||
@@ -145,7 +150,12 @@ class ScheduledActivitiesViewModel
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
unawaited(_tracking.legacyDeviceScheduledActivityUpdated());
|
||||
unawaited(
|
||||
_tracking.legacyDeviceScheduledActivityUpdated(
|
||||
weekDay: weekDay,
|
||||
period: period,
|
||||
),
|
||||
);
|
||||
|
||||
await _reload();
|
||||
} catch (e) {
|
||||
|
||||
@@ -53,6 +53,14 @@ class VolumeControlViewModel extends Notifier<VolumeControlViewState> {
|
||||
final device = state.device;
|
||||
if (device == null) return;
|
||||
|
||||
// Capture which volume types actually changed compared to the device's
|
||||
// previous settings, so we only emit analytics for the ones the user
|
||||
// actually moved.
|
||||
final previous = device.settings.volume;
|
||||
final mediaChanged = previous.media != state.media;
|
||||
final ringtoneChanged = previous.ringtone != state.ringtone;
|
||||
final alarmChanged = previous.alarm != state.alarm;
|
||||
|
||||
try {
|
||||
state = state.copyWith(
|
||||
isLoading: true,
|
||||
@@ -76,12 +84,30 @@ class VolumeControlViewModel extends Notifier<VolumeControlViewState> {
|
||||
if (!ref.mounted) return;
|
||||
ref.syncDeviceSettings(device, updatedSettings);
|
||||
|
||||
unawaited(
|
||||
_tracking.legacyDeviceVolumeControlChanged(
|
||||
type: 'media',
|
||||
level: state.media,
|
||||
),
|
||||
);
|
||||
if (mediaChanged) {
|
||||
unawaited(
|
||||
_tracking.legacyDeviceVolumeControlChanged(
|
||||
type: 'media',
|
||||
level: state.media,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (ringtoneChanged) {
|
||||
unawaited(
|
||||
_tracking.legacyDeviceVolumeControlChanged(
|
||||
type: 'ringtone',
|
||||
level: state.ringtone,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (alarmChanged) {
|
||||
unawaited(
|
||||
_tracking.legacyDeviceVolumeControlChanged(
|
||||
type: 'alarm',
|
||||
level: state.alarm,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
state = state.copyWith(isLoading: false, isComplete: true);
|
||||
} catch (e) {
|
||||
|
||||
@@ -28,12 +28,18 @@ class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
|
||||
late final TextEditingController watchCodeController;
|
||||
late final TextEditingController activationKeyController;
|
||||
|
||||
/// When the user entered the current step. Used to compute how long they
|
||||
/// spent on each step and fire `step_completed` with a `duration_seconds`
|
||||
/// param — super useful for marketing/UX to see where users get stuck.
|
||||
DateTime _currentStepEnteredAt = DateTime.now();
|
||||
|
||||
@override
|
||||
LegacyDeviceSetupViewState build() {
|
||||
final initial = const LegacyDeviceSetupViewState();
|
||||
_initControllers(initial);
|
||||
_addListeners();
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
_currentStepEnteredAt = DateTime.now();
|
||||
|
||||
unawaited(_tracking.legacyDeviceSetupStarted());
|
||||
|
||||
@@ -42,6 +48,19 @@ class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
|
||||
return initial;
|
||||
}
|
||||
|
||||
/// Emits a `step_completed` event for the step the user is LEAVING and
|
||||
/// resets the step timer so the next step starts fresh.
|
||||
void _completeCurrentStep(String stepName) {
|
||||
final duration = DateTime.now().difference(_currentStepEnteredAt).inSeconds;
|
||||
unawaited(
|
||||
_tracking.legacyDeviceSetupStepCompleted(
|
||||
step: stepName,
|
||||
durationSeconds: duration,
|
||||
),
|
||||
);
|
||||
_currentStepEnteredAt = DateTime.now();
|
||||
}
|
||||
|
||||
void _initControllers(LegacyDeviceSetupViewState s) {
|
||||
_deviceSetupRepository = ref.read(legacyDeviceSetupRepositoryProvider);
|
||||
|
||||
@@ -69,11 +88,11 @@ class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
|
||||
Future<void> next() async {
|
||||
switch (state.step) {
|
||||
case LegacyAddKidStep.intro:
|
||||
unawaited(_tracking.legacyDeviceSetupStepCompleted('intro'));
|
||||
_completeCurrentStep('intro');
|
||||
state = state.copyWith(step: LegacyAddKidStep.linkInfo);
|
||||
return;
|
||||
case LegacyAddKidStep.linkInfo:
|
||||
unawaited(_tracking.legacyDeviceSetupStepCompleted('link_info'));
|
||||
_completeCurrentStep('link_info');
|
||||
state = state.copyWith(step: LegacyAddKidStep.scanWatch);
|
||||
return;
|
||||
case LegacyAddKidStep.scanWatch:
|
||||
@@ -85,6 +104,11 @@ class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
|
||||
state = state.copyWith(errorMessage: I18n.errorScanWatchRequired);
|
||||
return;
|
||||
}
|
||||
// Distinguish QR vs manual code entry: if watchCode is populated and
|
||||
// watchQr is empty, the user typed it manually.
|
||||
if (state.watchCode.isNotEmpty && state.watchQr.isEmpty) {
|
||||
unawaited(_tracking.legacyDeviceSetupManualCodeEntered());
|
||||
}
|
||||
await _generateActivationKey(identificator);
|
||||
return;
|
||||
case LegacyAddKidStep.profile:
|
||||
@@ -99,7 +123,7 @@ class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
|
||||
identificator: identificator,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
unawaited(_tracking.legacyDeviceSetupStepCompleted('scan_watch'));
|
||||
_completeCurrentStep('scan_watch');
|
||||
state = state.copyWith(isLoading: false, step: LegacyAddKidStep.profile);
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
@@ -133,6 +157,10 @@ class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
|
||||
}
|
||||
|
||||
void onWatchQrScanned(String qr) {
|
||||
unawaited(_tracking.legacyDeviceSetupQrScanned());
|
||||
// The QR scan jumps directly from scan_watch to profile, so complete
|
||||
// the scan_watch step with its timer too.
|
||||
_completeCurrentStep('scan_watch');
|
||||
state = state.copyWith(watchQr: qr, step: LegacyAddKidStep.profile);
|
||||
}
|
||||
|
||||
@@ -173,7 +201,22 @@ class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
|
||||
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
unawaited(_tracking.legacyDeviceSetupCompleted());
|
||||
final now = DateTime.now();
|
||||
final ageYears =
|
||||
now.year -
|
||||
birth.year -
|
||||
((now.month < birth.month ||
|
||||
(now.month == birth.month && now.day < birth.day))
|
||||
? 1
|
||||
: 0);
|
||||
|
||||
unawaited(
|
||||
_tracking.legacyDeviceSetupCompleted(
|
||||
childGender: genrer,
|
||||
relationType: relationType,
|
||||
childAgeYears: ageYears,
|
||||
),
|
||||
);
|
||||
|
||||
state = state.copyWith(isLoading: false, isSuccess: true);
|
||||
return true;
|
||||
@@ -285,6 +328,9 @@ class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
|
||||
}
|
||||
|
||||
void resetForNewKid() {
|
||||
unawaited(_tracking.legacyDeviceSetupResetForNewKid());
|
||||
_currentStepEnteredAt = DateTime.now();
|
||||
|
||||
firstNameController.clear();
|
||||
lastNameController.clear();
|
||||
bornAtController.clear();
|
||||
|
||||
@@ -91,6 +91,8 @@ class LegacyLinkPhoneViewModel extends Notifier<LegacyLinkPhoneViewState> {
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
|
||||
unawaited(_tracking.legacyAuthLinkPhoneCodeRequestFailed(e.toString()));
|
||||
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: e.toString(),
|
||||
@@ -142,6 +144,10 @@ class LegacyLinkPhoneViewModel extends Notifier<LegacyLinkPhoneViewState> {
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
|
||||
unawaited(
|
||||
_tracking.legacyAuthLinkPhoneCodeVerificationFailed(e.toString()),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: e.toString(),
|
||||
|
||||
@@ -179,6 +179,7 @@ class LegacyRecoverPasswordViewModel
|
||||
final String password = state.password;
|
||||
|
||||
if (!state.equalPasswords) {
|
||||
unawaited(_tracking.legacyAuthPasswordResetFailed('unequal_passwords'));
|
||||
state = state.copyWith(
|
||||
errorMessage: 'errorMessageUnequalPasswords',
|
||||
passwordChanged: false,
|
||||
@@ -187,6 +188,7 @@ class LegacyRecoverPasswordViewModel
|
||||
}
|
||||
|
||||
if (!state.securityChecks['min']!) {
|
||||
unawaited(_tracking.legacyAuthPasswordResetFailed('too_short'));
|
||||
state = state.copyWith(
|
||||
errorMessage: 'errorMessagePasswordTooShort',
|
||||
passwordChanged: false,
|
||||
@@ -195,6 +197,7 @@ class LegacyRecoverPasswordViewModel
|
||||
}
|
||||
|
||||
if (!state.securityChecks['capital']!) {
|
||||
unawaited(_tracking.legacyAuthPasswordResetFailed('no_capitals'));
|
||||
state = state.copyWith(
|
||||
errorMessage: 'errorMessagePasswordNoCapitals',
|
||||
passwordChanged: false,
|
||||
@@ -203,6 +206,7 @@ class LegacyRecoverPasswordViewModel
|
||||
}
|
||||
|
||||
if (!state.securityChecks['number']!) {
|
||||
unawaited(_tracking.legacyAuthPasswordResetFailed('no_numbers'));
|
||||
state = state.copyWith(
|
||||
errorMessage: 'errorMessagePasswordNoNumbers',
|
||||
passwordChanged: false,
|
||||
@@ -211,6 +215,7 @@ class LegacyRecoverPasswordViewModel
|
||||
}
|
||||
|
||||
if (!state.securityChecks['special']!) {
|
||||
unawaited(_tracking.legacyAuthPasswordResetFailed('no_special_chars'));
|
||||
state = state.copyWith(
|
||||
errorMessage: 'errorMessagePasswordNoSpecialChars',
|
||||
passwordChanged: false,
|
||||
@@ -224,8 +229,10 @@ class LegacyRecoverPasswordViewModel
|
||||
newPassword: password,
|
||||
token: state.token,
|
||||
);
|
||||
unawaited(_tracking.legacyAuthPasswordResetCompleted());
|
||||
state = state.copyWith(isLoading: false, passwordChanged: true);
|
||||
} catch (error) {
|
||||
unawaited(_tracking.legacyAuthPasswordResetFailed(error.toString()));
|
||||
state = state.copyWith(
|
||||
errorMessage: error.toString(),
|
||||
isLoading: false,
|
||||
|
||||
@@ -348,22 +348,28 @@ class LegacySignUpViewModel extends Notifier<LegacySignUpViewState>
|
||||
void next() {
|
||||
if (state.isLoading) return;
|
||||
|
||||
final ok = switch (state.currentIndex) {
|
||||
final currentStep = state.currentIndex;
|
||||
final ok = switch (currentStep) {
|
||||
0 => _validateStep0(),
|
||||
1 => _validateStep1(),
|
||||
2 => _validateStep2(),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if (!ok) return;
|
||||
if (!ok) {
|
||||
unawaited(_tracking.legacyAuthSignupStepValidationFailed(currentStep));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.currentIndex >= _lastIndex) {
|
||||
unawaited(_tracking.legacyAuthSignupStepCompleted(currentStep));
|
||||
|
||||
if (currentStep >= _lastIndex) {
|
||||
unawaited(signUp());
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
currentIndex: (state.currentIndex + 1).clamp(0, _lastIndex),
|
||||
currentIndex: (currentStep + 1).clamp(0, _lastIndex),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -371,6 +377,8 @@ class LegacySignUpViewModel extends Notifier<LegacySignUpViewState>
|
||||
if (state.isLoading) return;
|
||||
if (state.currentIndex <= 0) return;
|
||||
|
||||
unawaited(_tracking.legacyAuthSignupStepBack(state.currentIndex));
|
||||
|
||||
state = state.copyWith(
|
||||
currentIndex: (state.currentIndex - 1).clamp(0, _lastIndex),
|
||||
);
|
||||
|
||||
@@ -16,12 +16,21 @@ final locationMapViewModelProvider =
|
||||
class LocationMapViewModel extends Notifier<LocationMapViewState> {
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
/// Debounce timer for map zoom tracking. We do NOT want to fire an event
|
||||
/// for every zoom delta during a pinch gesture; we wait until the user
|
||||
/// stops zooming for a brief moment and then log the final zoom level.
|
||||
Timer? _zoomDebounce;
|
||||
static const Duration _zoomDebounceDelay = Duration(seconds: 1);
|
||||
|
||||
@override
|
||||
LocationMapViewState build() {
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
ref.onDispose(() => _zoomDebounce?.cancel());
|
||||
return const LocationMapViewState();
|
||||
}
|
||||
|
||||
// ─── Layer toggles ─────────────────────────────────────────────────────
|
||||
|
||||
void toggleGeofences() {
|
||||
final newVisible = !state.showGeofences;
|
||||
unawaited(_tracking.legacyLocationMapGeofencesToggled(newVisible));
|
||||
@@ -34,11 +43,33 @@ class LocationMapViewModel extends Notifier<LocationMapViewState> {
|
||||
state = state.copyWith(showFrequentPlaces: newVisible);
|
||||
}
|
||||
|
||||
// ─── Place creation funnel ────────────────────────────────────────────
|
||||
|
||||
void startPlacing(PlacingMode mode) {
|
||||
unawaited(_tracking.legacyLocationPlaceCreationStarted(_modeName(mode)));
|
||||
state = state.copyWith(placingMode: mode);
|
||||
}
|
||||
|
||||
void cancelPlacing() {
|
||||
// Infer mode + step from current state BEFORE we reset it so we can
|
||||
// send meaningful funnel drop-off data to analytics.
|
||||
final wasPicking = state.placingMode != PlacingMode.none;
|
||||
final wasAdjustingRadius = state.adjustingRadius;
|
||||
|
||||
if (wasPicking || wasAdjustingRadius) {
|
||||
final mode = wasPicking
|
||||
? _modeName(state.placingMode)
|
||||
// Only geofences reach the radius-adjust step.
|
||||
: 'geofence';
|
||||
final atStep = wasPicking ? 'picking_point' : 'adjusting_radius';
|
||||
unawaited(
|
||||
_tracking.legacyLocationPlaceCreationCancelled(
|
||||
mode: mode,
|
||||
atStep: atStep,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
placingMode: PlacingMode.none,
|
||||
adjustingRadius: false,
|
||||
@@ -47,6 +78,7 @@ class LocationMapViewModel extends Notifier<LocationMapViewState> {
|
||||
}
|
||||
|
||||
void confirmGeofencePlacement(LatLng center) {
|
||||
unawaited(_tracking.legacyLocationPointConfirmed('geofence'));
|
||||
state = state.copyWith(
|
||||
placingMode: PlacingMode.none,
|
||||
adjustingRadius: true,
|
||||
@@ -56,14 +88,24 @@ class LocationMapViewModel extends Notifier<LocationMapViewState> {
|
||||
}
|
||||
|
||||
void confirmFrequentPlacePlacement() {
|
||||
unawaited(_tracking.legacyLocationPointConfirmed('frequent_place'));
|
||||
state = state.copyWith(placingMode: PlacingMode.none, previewPoint: null);
|
||||
}
|
||||
|
||||
void updatePreviewRadius(double radius) {
|
||||
// Intentionally NOT tracked: this fires on every slider tick. The final
|
||||
// radius is captured in [confirmRadius] once the user settles.
|
||||
state = state.copyWith(previewRadius: radius);
|
||||
}
|
||||
|
||||
void confirmRadius() {
|
||||
final isEditing = state.editingGeofence != null;
|
||||
unawaited(
|
||||
_tracking.legacyLocationRadiusConfirmed(
|
||||
radius: state.previewRadius,
|
||||
isEditing: isEditing,
|
||||
),
|
||||
);
|
||||
state = state.copyWith(adjustingRadius: false, editingGeofence: null);
|
||||
}
|
||||
|
||||
@@ -71,7 +113,10 @@ class LocationMapViewModel extends Notifier<LocationMapViewState> {
|
||||
state = state.copyWith(previewPoint: null);
|
||||
}
|
||||
|
||||
// ─── Exploration / selection ──────────────────────────────────────────
|
||||
|
||||
void selectGeofence(GeofenceEntity geofence) {
|
||||
unawaited(_tracking.legacyLocationGeofenceSelected());
|
||||
state = state.copyWith(
|
||||
selectedGeofence: geofence,
|
||||
selectedFrequentPlace: null,
|
||||
@@ -79,10 +124,14 @@ class LocationMapViewModel extends Notifier<LocationMapViewState> {
|
||||
}
|
||||
|
||||
void clearSelectedGeofence() {
|
||||
if (state.selectedGeofence != null) {
|
||||
unawaited(_tracking.legacyLocationGeofenceDismissed());
|
||||
}
|
||||
state = state.copyWith(selectedGeofence: null);
|
||||
}
|
||||
|
||||
void startEditingGeofence(GeofenceEntity geofence) {
|
||||
unawaited(_tracking.legacyLocationGeofenceEditStarted());
|
||||
state = state.copyWith(
|
||||
selectedGeofence: null,
|
||||
editingGeofence: geofence,
|
||||
@@ -93,6 +142,7 @@ class LocationMapViewModel extends Notifier<LocationMapViewState> {
|
||||
}
|
||||
|
||||
void selectFrequentPlace(FrequentPlaceEntity place) {
|
||||
unawaited(_tracking.legacyLocationFrequentPlaceSelected());
|
||||
state = state.copyWith(
|
||||
selectedFrequentPlace: place,
|
||||
selectedGeofence: null,
|
||||
@@ -100,27 +150,47 @@ class LocationMapViewModel extends Notifier<LocationMapViewState> {
|
||||
}
|
||||
|
||||
void clearSelectedFrequentPlace() {
|
||||
if (state.selectedFrequentPlace != null) {
|
||||
unawaited(_tracking.legacyLocationFrequentPlaceDismissed());
|
||||
}
|
||||
state = state.copyWith(selectedFrequentPlace: null);
|
||||
}
|
||||
|
||||
void selectHistoryPosition(PositionEntity position) {
|
||||
unawaited(_tracking.legacyLocationHistoryPositionSelected());
|
||||
state = state.copyWith(selectedHistoryPosition: position);
|
||||
}
|
||||
|
||||
void clearSelectedHistoryPosition() {
|
||||
if (state.selectedHistoryPosition != null) {
|
||||
unawaited(_tracking.legacyLocationHistoryPositionDismissed());
|
||||
}
|
||||
state = state.copyWith(selectedHistoryPosition: null);
|
||||
}
|
||||
|
||||
// ─── Live tracking ────────────────────────────────────────────────────
|
||||
|
||||
void toggleFollowing() {
|
||||
state = state.copyWith(isFollowing: !state.isFollowing);
|
||||
final newFollowing = !state.isFollowing;
|
||||
unawaited(_tracking.legacyLocationFollowingToggled(newFollowing));
|
||||
state = state.copyWith(isFollowing: newFollowing);
|
||||
}
|
||||
|
||||
void stopFollowing() {
|
||||
// Called automatically when the user pans the map manually. We track
|
||||
// this as a "following disabled" event for consistency with toggle.
|
||||
if (state.isFollowing) {
|
||||
unawaited(_tracking.legacyLocationFollowingToggled(false));
|
||||
}
|
||||
state = state.copyWith(isFollowing: false);
|
||||
}
|
||||
|
||||
// ─── UI chrome ────────────────────────────────────────────────────────
|
||||
|
||||
void toggleActionsExpanded() {
|
||||
state = state.copyWith(actionsExpanded: !state.actionsExpanded);
|
||||
final newExpanded = !state.actionsExpanded;
|
||||
unawaited(_tracking.legacyLocationMapActionsExpanded(newExpanded));
|
||||
state = state.copyWith(actionsExpanded: newExpanded);
|
||||
}
|
||||
|
||||
void toggleFrequencyExpanded() {
|
||||
@@ -131,7 +201,30 @@ class LocationMapViewModel extends Notifier<LocationMapViewState> {
|
||||
state = state.copyWith(frequencyExpanded: false);
|
||||
}
|
||||
|
||||
// ─── Map zoom (debounced) ─────────────────────────────────────────────
|
||||
|
||||
void updateMapZoom(double zoom) {
|
||||
state = state.copyWith(mapZoom: zoom);
|
||||
|
||||
// Debounce: cancel any pending fire and schedule a new one. If the user
|
||||
// keeps zooming, the timer keeps resetting and we only fire once the
|
||||
// zoom settles for [_zoomDebounceDelay].
|
||||
_zoomDebounce?.cancel();
|
||||
_zoomDebounce = Timer(_zoomDebounceDelay, () {
|
||||
unawaited(_tracking.legacyLocationMapZoomed(zoom));
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
String _modeName(PlacingMode mode) {
|
||||
switch (mode) {
|
||||
case PlacingMode.geofence:
|
||||
return 'geofence';
|
||||
case PlacingMode.frequentPlace:
|
||||
return 'frequent_place';
|
||||
case PlacingMode.none:
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +309,9 @@ class LocationViewModel extends Notifier<LocationViewState> {
|
||||
}
|
||||
|
||||
void clearPositionHistory() {
|
||||
if (state.positionHistory.isNotEmpty) {
|
||||
unawaited(_tracking.legacyLocationHistoryCleared());
|
||||
}
|
||||
state = state.copyWith(positionHistory: [], showRouteTrail: false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:control_panel/control_panel.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:location/src/core/domain/entities/geofence_entity.dart';
|
||||
import 'package:location/src/core/domain/entities/frequent_place_entity.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
class LocationListSheet extends StatefulWidget {
|
||||
class LocationListSheet extends ConsumerStatefulWidget {
|
||||
final List<GeofenceEntity> geofences;
|
||||
final List<FrequentPlaceEntity> frequentPlaces;
|
||||
final List<PositionEntity> positionHistory;
|
||||
@@ -25,10 +29,10 @@ class LocationListSheet extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<LocationListSheet> createState() => _LocationListSheetState();
|
||||
ConsumerState<LocationListSheet> createState() => _LocationListSheetState();
|
||||
}
|
||||
|
||||
class _LocationListSheetState extends State<LocationListSheet> {
|
||||
class _LocationListSheetState extends ConsumerState<LocationListSheet> {
|
||||
String? _selectedType;
|
||||
|
||||
List<PositionEntity> get _filteredHistory {
|
||||
@@ -170,7 +174,14 @@ class _LocationListSheetState extends State<LocationListSheet> {
|
||||
Widget _buildFilterChip({required String label, required String? value}) {
|
||||
final isSelected = _selectedType == value;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _selectedType = value),
|
||||
onTap: () {
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacyLocationHistoryTypeFilterChanged(value ?? 'all'),
|
||||
);
|
||||
setState(() => _selectedType = value);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:location/src/features/location/presentation/state/location_map_v
|
||||
import 'package:location/src/features/location/presentation/state/location_view_model.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';
|
||||
|
||||
import 'device_banner.dart';
|
||||
@@ -162,6 +163,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
|
||||
void _centerOnDevice() {
|
||||
if (widget.selectedPosition == null) return;
|
||||
unawaited(
|
||||
ref.read(sfTrackingProvider).legacyLocationMapCenterTapped(),
|
||||
);
|
||||
_animatedMove(
|
||||
LatLng(
|
||||
widget.selectedPosition!.latitude,
|
||||
@@ -217,6 +221,8 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
if (addressStr.isNotEmpty) text.writeln(addressStr);
|
||||
text.writeln(mapsUrl);
|
||||
|
||||
unawaited(ref.read(sfTrackingProvider).legacyLocationShared());
|
||||
|
||||
Share.share(text.toString().trim());
|
||||
}
|
||||
|
||||
@@ -318,6 +324,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
}
|
||||
|
||||
void _showListSheet() {
|
||||
unawaited(ref.read(sfTrackingProvider).legacyLocationListSheetOpened());
|
||||
final locationState = ref.read(locationViewModelProvider);
|
||||
final mapState = ref.read(locationMapViewModelProvider);
|
||||
showModalBottomSheet(
|
||||
@@ -650,7 +657,12 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
onAddGeofence: () => _vm.startPlacing(PlacingMode.geofence),
|
||||
onAddFrequentPlace: () => _vm.startPlacing(PlacingMode.frequentPlace),
|
||||
onShareTap: _shareLocation,
|
||||
onRefreshTap: widget.onRefreshPosition,
|
||||
onRefreshTap: () {
|
||||
unawaited(
|
||||
ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(),
|
||||
);
|
||||
widget.onRefreshPosition();
|
||||
},
|
||||
onCenterTap: _centerOnDevice,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_shared/legacy_shared.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
const _labelKeys = {
|
||||
MapStyle.standard: I18n.locationMapStyleStandard,
|
||||
@@ -65,6 +68,11 @@ class _MapStyleSelectorState extends ConsumerState<MapStyleSelector> {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
ref.read(mapStyleProvider.notifier).setStyle(style);
|
||||
unawaited(
|
||||
ref
|
||||
.read(sfTrackingProvider)
|
||||
.legacyLocationMapStyleChanged(style.name),
|
||||
);
|
||||
setState(() => _expanded = false);
|
||||
},
|
||||
child: Container(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_shared/legacy_shared.dart';
|
||||
import 'package:settings/src/core/data/models/create_alarm_request_model.dart';
|
||||
@@ -29,6 +30,12 @@ class AlarmViewModel extends Notifier<AlarmViewState> {
|
||||
return const AlarmViewState();
|
||||
}
|
||||
|
||||
String _formatTime(TimeOfDay time) {
|
||||
final hh = time.hour.toString().padLeft(2, '0');
|
||||
final mm = time.minute.toString().padLeft(2, '0');
|
||||
return '$hh:$mm';
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final device = ref.read(selectedDeviceProvider);
|
||||
@@ -68,7 +75,9 @@ class AlarmViewModel extends Notifier<AlarmViewState> {
|
||||
order: state.alarms.length + 1,
|
||||
);
|
||||
|
||||
unawaited(_tracking.legacySettingsAlarmAdded());
|
||||
unawaited(
|
||||
_tracking.legacySettingsAlarmAdded(time: _formatTime(alarm.time)),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
alarms: [...state.alarms, updatedAlarm],
|
||||
@@ -100,7 +109,9 @@ class AlarmViewModel extends Notifier<AlarmViewState> {
|
||||
.map((a) => a.id == alarm.id ? alarm : a)
|
||||
.toList();
|
||||
|
||||
unawaited(_tracking.legacySettingsAlarmUpdated());
|
||||
unawaited(
|
||||
_tracking.legacySettingsAlarmUpdated(time: _formatTime(alarm.time)),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
alarms: updatedAlarms,
|
||||
|
||||
@@ -68,7 +68,16 @@ class AlertsViewModel extends Notifier<AlertsViewState> {
|
||||
if (!ref.mounted) return;
|
||||
ref.syncDeviceSettings(device, updatedSettings);
|
||||
|
||||
unawaited(_tracking.legacySettingsAlertsConfigured());
|
||||
final alertsJoined = state.activeAlerts.join(',');
|
||||
final truncated = alertsJoined.length > 100
|
||||
? alertsJoined.substring(0, 100)
|
||||
: alertsJoined;
|
||||
unawaited(
|
||||
_tracking.legacySettingsAlertsConfigured(
|
||||
alertCount: state.activeAlerts.length,
|
||||
alertsEnabled: truncated,
|
||||
),
|
||||
);
|
||||
|
||||
state = state.copyWith(isSaving: false, saveSuccess: true);
|
||||
} catch (e) {
|
||||
|
||||
@@ -56,7 +56,11 @@ class BlockPhoneViewModel extends Notifier<BlockPhoneViewState> {
|
||||
contacts: updatedContacts,
|
||||
);
|
||||
|
||||
unawaited(_tracking.legacySettingsBlockPhoneContactAdded());
|
||||
unawaited(
|
||||
_tracking.legacySettingsBlockPhoneContactAdded(
|
||||
totalCount: updatedContacts.length,
|
||||
),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
contacts: updatedContacts,
|
||||
@@ -86,7 +90,11 @@ class BlockPhoneViewModel extends Notifier<BlockPhoneViewState> {
|
||||
contacts: updatedContacts,
|
||||
);
|
||||
|
||||
unawaited(_tracking.legacySettingsBlockPhoneContactRemoved());
|
||||
unawaited(
|
||||
_tracking.legacySettingsBlockPhoneContactRemoved(
|
||||
totalCount: updatedContacts.length,
|
||||
),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
contacts: updatedContacts,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_shared/legacy_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
import 'remote_management_view_state.dart';
|
||||
|
||||
final remoteManagementViewModelProvider =
|
||||
@@ -10,10 +14,12 @@ final remoteManagementViewModelProvider =
|
||||
|
||||
class RemoteManagementViewModel extends Notifier<RemoteManagementViewState> {
|
||||
late final CommandsRepository _commandsRepository;
|
||||
late final SfTrackingRepository _tracking;
|
||||
|
||||
@override
|
||||
RemoteManagementViewState build() {
|
||||
_commandsRepository = ref.read(commandsRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
|
||||
state = const RemoteManagementViewState();
|
||||
|
||||
@@ -46,14 +52,17 @@ class RemoteManagementViewModel extends Notifier<RemoteManagementViewState> {
|
||||
}
|
||||
|
||||
Future<void> shutdown() async {
|
||||
unawaited(_tracking.legacySettingsRemoteManagementShutdown());
|
||||
await tryCommand(DeviceCommand.shutdown);
|
||||
}
|
||||
|
||||
Future<void> restart() async {
|
||||
unawaited(_tracking.legacySettingsRemoteManagementRestart());
|
||||
await tryCommand(DeviceCommand.restart);
|
||||
}
|
||||
|
||||
Future<void> factoryReset() async {
|
||||
unawaited(_tracking.legacySettingsRemoteManagementFactoryReset());
|
||||
await tryCommand(DeviceCommand.factory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,11 @@ class SosContactsViewModel extends Notifier<SosContactsViewState> {
|
||||
contacts: updatedContacts,
|
||||
);
|
||||
|
||||
unawaited(_tracking.legacySettingsSosContactAdded());
|
||||
unawaited(
|
||||
_tracking.legacySettingsSosContactAdded(
|
||||
totalCount: updatedContacts.length,
|
||||
),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
contacts: updatedContacts,
|
||||
@@ -88,7 +92,11 @@ class SosContactsViewModel extends Notifier<SosContactsViewState> {
|
||||
contacts: updatedContacts,
|
||||
);
|
||||
|
||||
unawaited(_tracking.legacySettingsSosContactRemoved());
|
||||
unawaited(
|
||||
_tracking.legacySettingsSosContactRemoved(
|
||||
totalCount: updatedContacts.length,
|
||||
),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
contacts: updatedContacts,
|
||||
|
||||
@@ -67,7 +67,9 @@ class SoundViewModel extends Notifier<SoundViewState> {
|
||||
if (!ref.mounted) return;
|
||||
ref.syncDeviceSettings(device, updatedSettings);
|
||||
|
||||
unawaited(_tracking.legacySettingsSoundChanged());
|
||||
unawaited(
|
||||
_tracking.legacySettingsSoundChanged(mode: state.soundOption ?? ''),
|
||||
);
|
||||
|
||||
state = state.copyWith(isLoading: false, isComplete: true);
|
||||
} catch (e) {
|
||||
|
||||
@@ -56,7 +56,9 @@ class WifiSettingsViewModel extends Notifier<WifiSettingsViewState> {
|
||||
|
||||
final networks = await _repository.getWifiNetworks(deviceId: device.id);
|
||||
|
||||
unawaited(_tracking.legacySettingsWifiAdded());
|
||||
unawaited(
|
||||
_tracking.legacySettingsWifiAdded(totalCount: networks.length),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
networks: networks,
|
||||
@@ -82,7 +84,9 @@ class WifiSettingsViewModel extends Notifier<WifiSettingsViewState> {
|
||||
|
||||
final networks = await _repository.getWifiNetworks(deviceId: device.id);
|
||||
|
||||
unawaited(_tracking.legacySettingsWifiRemoved());
|
||||
unawaited(
|
||||
_tracking.legacySettingsWifiRemoved(totalCount: networks.length),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
networks: networks,
|
||||
|
||||
@@ -21,6 +21,17 @@ mixin AccountTracking on Tracking {
|
||||
Future<void> legacyAccountLinkedDeviceUnlinked() =>
|
||||
trackEvent('${_prefix}_linked_device_unlinked');
|
||||
|
||||
/// User renamed a linked device (edited the carrier name on the linked
|
||||
/// devices screen).
|
||||
Future<void> legacyAccountLinkedDeviceRenamed() =>
|
||||
trackEvent('${_prefix}_linked_device_renamed');
|
||||
|
||||
/// User tapped "delete" on the app users screen. Note: the current
|
||||
/// implementation is known to delete the logged user (placeholder logic);
|
||||
/// the event is still useful to measure demand for this feature.
|
||||
Future<void> legacyAccountAppUserDeleteTriggered() =>
|
||||
trackEvent('${_prefix}_app_user_delete_triggered');
|
||||
|
||||
/// ⚠️ CHURN SIGNAL — User opened the account deletion flow.
|
||||
Future<void> legacyAccountDeletionInitiated() =>
|
||||
trackEvent('${_prefix}_deletion_initiated');
|
||||
|
||||
@@ -46,6 +46,25 @@ mixin AuthTracking on Tracking {
|
||||
Future<void> legacyAuthSignupFailed(String reason) =>
|
||||
trackEvent('${_prefix}_signup_failed', {'reason': reason});
|
||||
|
||||
/// User successfully advanced past a signup form step (validation passed
|
||||
/// and they moved to the next page). [stepIndex] is 0-based.
|
||||
Future<void> legacyAuthSignupStepCompleted(int stepIndex) =>
|
||||
trackEvent('${_prefix}_signup_step_completed', {
|
||||
'step_index': stepIndex,
|
||||
});
|
||||
|
||||
/// User went back to a previous signup step. [stepIndex] is the step they
|
||||
/// JUST LEFT (i.e. the one they came from).
|
||||
Future<void> legacyAuthSignupStepBack(int stepIndex) =>
|
||||
trackEvent('${_prefix}_signup_step_back', {'step_index': stepIndex});
|
||||
|
||||
/// User tried to advance past a step but client-side validation failed.
|
||||
/// [stepIndex] is 0-based.
|
||||
Future<void> legacyAuthSignupStepValidationFailed(int stepIndex) =>
|
||||
trackEvent('${_prefix}_signup_step_validation_failed', {
|
||||
'step_index': stepIndex,
|
||||
});
|
||||
|
||||
// ─── Password recovery ───────────────────────────────────────────────────
|
||||
|
||||
/// User requested a password reset (form submitted, request in flight).
|
||||
@@ -56,16 +75,38 @@ mixin AuthTracking on Tracking {
|
||||
Future<void> legacyAuthPasswordResetEmailSent() =>
|
||||
trackEvent('${_prefix}_password_reset_email_sent');
|
||||
|
||||
/// ⭐ User finished the password recovery flow (new password saved on
|
||||
/// backend). This is the final conversion of the recovery funnel.
|
||||
Future<void> legacyAuthPasswordResetCompleted() =>
|
||||
trackEvent('${_prefix}_password_reset_completed');
|
||||
|
||||
/// Password recovery failed — either client validation (weak password) or
|
||||
/// backend error.
|
||||
Future<void> legacyAuthPasswordResetFailed(String reason) =>
|
||||
trackEvent('${_prefix}_password_reset_failed', {'reason': reason});
|
||||
|
||||
// ─── Phone linking ───────────────────────────────────────────────────────
|
||||
|
||||
/// User submitted a phone number to receive an OTP code for phone linking.
|
||||
Future<void> legacyAuthLinkPhoneCodeRequested() =>
|
||||
trackEvent('${_prefix}_link_phone_code_requested');
|
||||
|
||||
/// Requesting the OTP code from the backend failed.
|
||||
Future<void> legacyAuthLinkPhoneCodeRequestFailed(String reason) =>
|
||||
trackEvent('${_prefix}_link_phone_code_request_failed', {
|
||||
'reason': reason,
|
||||
});
|
||||
|
||||
/// User successfully verified the OTP code and the phone is linked.
|
||||
Future<void> legacyAuthLinkPhoneCodeVerified() =>
|
||||
trackEvent('${_prefix}_link_phone_code_verified');
|
||||
|
||||
/// OTP verification for phone linking failed (wrong code, expired, etc).
|
||||
Future<void> legacyAuthLinkPhoneCodeVerificationFailed(String reason) =>
|
||||
trackEvent('${_prefix}_link_phone_code_verification_failed', {
|
||||
'reason': reason,
|
||||
});
|
||||
|
||||
// ─── Logout ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// User logged out and the session has been cleared.
|
||||
|
||||
@@ -5,12 +5,17 @@ const _prefix = 'legacy_contacts';
|
||||
/// Tracking events for the legacy device-contacts feature (contacts allowed
|
||||
/// to call/be called by the kid's device). Mixed into `SfTrackingRepository`.
|
||||
mixin ContactsTracking on Tracking {
|
||||
/// User created a new contact entry on the device.
|
||||
Future<void> legacyContactsAdded() => trackEvent('${_prefix}_added');
|
||||
/// User created a new contact entry on the device. [totalCount] is the
|
||||
/// total number of contacts the user has AFTER the add (useful to segment
|
||||
/// by "small agenda vs. big agenda").
|
||||
Future<void> legacyContactsAdded({required int totalCount}) =>
|
||||
trackEvent('${_prefix}_added', {'total_count': totalCount});
|
||||
|
||||
/// User edited an existing contact entry.
|
||||
Future<void> legacyContactsEdited() => trackEvent('${_prefix}_edited');
|
||||
|
||||
/// User deleted a contact entry from the device.
|
||||
Future<void> legacyContactsDeleted() => trackEvent('${_prefix}_deleted');
|
||||
/// User deleted a contact entry from the device. [totalCount] is the total
|
||||
/// number of contacts remaining after the delete.
|
||||
Future<void> legacyContactsDeleted({required int totalCount}) =>
|
||||
trackEvent('${_prefix}_deleted', {'total_count': totalCount});
|
||||
}
|
||||
|
||||
@@ -6,8 +6,14 @@ const _prefix = 'legacy_control_panel';
|
||||
/// device cards and a position map). Mixed into `SfTrackingRepository`.
|
||||
mixin ControlPanelTracking on Tracking {
|
||||
/// User switched the active device (which kid they're monitoring).
|
||||
Future<void> legacyControlPanelDeviceSelected() =>
|
||||
trackEvent('${_prefix}_device_selected');
|
||||
/// [totalDevices] is how many devices the user has linked — a proxy for
|
||||
/// family size (more kids = stickier segment).
|
||||
Future<void> legacyControlPanelDeviceSelected({
|
||||
required int totalDevices,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_device_selected', {
|
||||
'total_devices': totalDevices,
|
||||
});
|
||||
|
||||
/// User triggered a manual refresh of device positions (pull-to-refresh
|
||||
/// or refresh button).
|
||||
|
||||
@@ -11,11 +11,30 @@ mixin DeviceSetupTracking on Tracking {
|
||||
|
||||
/// User completed an individual step of the wizard. [step] is the name of
|
||||
/// the step they just left (intro, link_info, scan_watch, profile, etc).
|
||||
Future<void> legacyDeviceSetupStepCompleted(String step) =>
|
||||
trackEvent('${_prefix}_step_completed', {'step': step});
|
||||
/// [durationSeconds] is how long they spent on that step.
|
||||
Future<void> legacyDeviceSetupStepCompleted({
|
||||
required String step,
|
||||
required int durationSeconds,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_step_completed', {
|
||||
'step': step,
|
||||
'duration_seconds': durationSeconds,
|
||||
});
|
||||
|
||||
/// User finished the wizard and the device was successfully created.
|
||||
Future<void> legacyDeviceSetupCompleted() => trackEvent('${_prefix}_completed');
|
||||
/// ⭐ User finished the wizard and the device was successfully created.
|
||||
/// Enriched with demographics of the kid: gender, relation type
|
||||
/// (mother/father/etc) and age in years — all super valuable for marketing
|
||||
/// personas.
|
||||
Future<void> legacyDeviceSetupCompleted({
|
||||
required String childGender,
|
||||
required String relationType,
|
||||
required int childAgeYears,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_completed', {
|
||||
'child_gender': childGender,
|
||||
'relation_type': relationType,
|
||||
'child_age_years': childAgeYears,
|
||||
});
|
||||
|
||||
/// Wizard failed to complete due to an error. [atStep] is the step where it
|
||||
/// failed and [reason] is the error message.
|
||||
@@ -32,4 +51,20 @@ mixin DeviceSetupTracking on Tracking {
|
||||
/// step they were on when they cancelled.
|
||||
Future<void> legacyDeviceSetupCancelled(String atStep) =>
|
||||
trackEvent('${_prefix}_cancelled', {'at_step': atStep});
|
||||
|
||||
/// User successfully scanned the watch QR code (instead of typing the code
|
||||
/// manually).
|
||||
Future<void> legacyDeviceSetupQrScanned() =>
|
||||
trackEvent('${_prefix}_qr_scanned');
|
||||
|
||||
/// User advanced past the scan-watch step using a manually typed code
|
||||
/// instead of scanning the QR.
|
||||
Future<void> legacyDeviceSetupManualCodeEntered() =>
|
||||
trackEvent('${_prefix}_manual_code_entered');
|
||||
|
||||
/// ⭐ After successfully creating a device, user tapped "add another kid"
|
||||
/// which resets the wizard. This is the strongest signal of families with
|
||||
/// multiple kids — a retention goldmine.
|
||||
Future<void> legacyDeviceSetupResetForNewKid() =>
|
||||
trackEvent('${_prefix}_reset_for_new_kid');
|
||||
}
|
||||
|
||||
@@ -3,21 +3,50 @@ import 'package:sf_tracking/src/tracking.dart';
|
||||
const _prefix = 'legacy_device';
|
||||
|
||||
/// Tracking events for the legacy device management module (locate, remote
|
||||
/// connection, volume, scheduled activities, rewards, background image).
|
||||
/// Mixed into `SfTrackingRepository`.
|
||||
/// connection, volume, scheduled activities, rewards, background image,
|
||||
/// health / steps / apps usage / call history). Mixed into
|
||||
/// `SfTrackingRepository`.
|
||||
///
|
||||
/// Pure screen views (health, apps_use, activity_meter, call_history,
|
||||
/// background_image viewing) are tracked automatically by `SfRouterListener`
|
||||
/// — only explicit user actions are listed here.
|
||||
/// Pure screen views are tracked automatically by `SfRouterListener` — only
|
||||
/// explicit user actions are listed here.
|
||||
mixin DeviceTracking on Tracking {
|
||||
// ─── Locate ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// User triggered a "locate device" command.
|
||||
Future<void> legacyDeviceLocateRequested() =>
|
||||
trackEvent('${_prefix}_locate_requested');
|
||||
|
||||
/// The locate command completed successfully on the backend.
|
||||
Future<void> legacyDeviceLocateSuccess() =>
|
||||
trackEvent('${_prefix}_locate_success');
|
||||
|
||||
/// The locate command failed.
|
||||
Future<void> legacyDeviceLocateFailure(String reason) =>
|
||||
trackEvent('${_prefix}_locate_failure', {'reason': reason});
|
||||
|
||||
// ─── Remote connection (camera + call) ───────────────────────────────────
|
||||
|
||||
/// User entered the remote connection screen (active session start).
|
||||
Future<void> legacyDeviceRemoteConnectionStarted() =>
|
||||
trackEvent('${_prefix}_remote_connection_started');
|
||||
|
||||
/// User triggered the remote camera to take a photo via remote connection.
|
||||
Future<void> legacyDeviceRemoteConnectionPhotoTaken() =>
|
||||
trackEvent('${_prefix}_remote_connection_photo_taken');
|
||||
|
||||
/// User initiated a two-way call from the remote connection screen.
|
||||
Future<void> legacyDeviceRemoteConnectionCallInitiated() =>
|
||||
trackEvent('${_prefix}_remote_connection_call_initiated');
|
||||
|
||||
/// User navigated through the pictures from the remote camera.
|
||||
/// [direction] is `next`, `prev` or `direct` (tapped a specific index).
|
||||
Future<void> legacyDeviceRemoteConnectionPictureViewed(String direction) =>
|
||||
trackEvent('${_prefix}_remote_connection_picture_viewed', {
|
||||
'direction': direction,
|
||||
});
|
||||
|
||||
// ─── Volume control ──────────────────────────────────────────────────────
|
||||
|
||||
/// User changed a volume control level. [type] is `media`, `ringtone` or
|
||||
/// `alarm`. [level] is the new value (0-100 typically).
|
||||
Future<void> legacyDeviceVolumeControlChanged({
|
||||
@@ -29,37 +58,51 @@ mixin DeviceTracking on Tracking {
|
||||
'level': level,
|
||||
});
|
||||
|
||||
/// User changed the device background image.
|
||||
// ─── Background image ────────────────────────────────────────────────────
|
||||
|
||||
/// User changed the device background image (chose from library).
|
||||
Future<void> legacyDeviceBackgroundImageChanged() =>
|
||||
trackEvent('${_prefix}_background_image_changed');
|
||||
|
||||
/// User added a scheduled activity (alarm, reminder, etc on the device).
|
||||
Future<void> legacyDeviceScheduledActivityAdded() =>
|
||||
trackEvent('${_prefix}_scheduled_activity_added');
|
||||
|
||||
/// User removed a scheduled activity.
|
||||
Future<void> legacyDeviceScheduledActivityRemoved() =>
|
||||
trackEvent('${_prefix}_scheduled_activity_removed');
|
||||
|
||||
/// User assigned reward minutes to the device.
|
||||
Future<void> legacyDeviceRewardsGranted({required int amount}) =>
|
||||
trackEvent('${_prefix}_rewards_granted', {'amount': amount});
|
||||
|
||||
/// User triggered the remote camera to take a photo via remote connection.
|
||||
Future<void> legacyDeviceRemoteConnectionPhotoTaken() =>
|
||||
trackEvent('${_prefix}_remote_connection_photo_taken');
|
||||
|
||||
/// User initiated a two-way call from the remote connection screen.
|
||||
Future<void> legacyDeviceRemoteConnectionCallInitiated() =>
|
||||
trackEvent('${_prefix}_remote_connection_call_initiated');
|
||||
|
||||
/// User uploaded a custom photo to use as device background.
|
||||
Future<void> legacyDeviceBackgroundImageUploaded() =>
|
||||
trackEvent('${_prefix}_background_image_uploaded');
|
||||
|
||||
// ─── Scheduled activities ────────────────────────────────────────────────
|
||||
|
||||
/// User added a scheduled activity (alarm, reminder, etc on the device).
|
||||
/// [weekDay] is 0-6 (0 = Sunday). [period] is a `HH:mm-HH:mm` string.
|
||||
Future<void> legacyDeviceScheduledActivityAdded({
|
||||
required int weekDay,
|
||||
required String period,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_scheduled_activity_added', {
|
||||
'week_day': weekDay,
|
||||
'period': period,
|
||||
});
|
||||
|
||||
/// User edited a previously created scheduled activity.
|
||||
Future<void> legacyDeviceScheduledActivityUpdated() =>
|
||||
trackEvent('${_prefix}_scheduled_activity_updated');
|
||||
/// [weekDay] 0-6, [period] `HH:mm-HH:mm` string.
|
||||
Future<void> legacyDeviceScheduledActivityUpdated({
|
||||
required int weekDay,
|
||||
required String period,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_scheduled_activity_updated', {
|
||||
'week_day': weekDay,
|
||||
'period': period,
|
||||
});
|
||||
|
||||
/// User removed a scheduled activity.
|
||||
Future<void> legacyDeviceScheduledActivityRemoved() =>
|
||||
trackEvent('${_prefix}_scheduled_activity_removed');
|
||||
|
||||
// ─── Rewards ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// User assigned reward minutes to the device.
|
||||
Future<void> legacyDeviceRewardsGranted({required int amount}) =>
|
||||
trackEvent('${_prefix}_rewards_granted', {'amount': amount});
|
||||
|
||||
// ─── Activity meter (steps) ──────────────────────────────────────────────
|
||||
|
||||
/// User toggled the pedometer (step counter) on the device.
|
||||
Future<void> legacyDeviceActivityPedometerToggled(bool enabled) =>
|
||||
@@ -67,6 +110,15 @@ mixin DeviceTracking on Tracking {
|
||||
'enabled': enabled.toString(),
|
||||
});
|
||||
|
||||
/// User changed the time range filter on the activity meter screen.
|
||||
/// [range] is `today`, `seven_days`, `thirty_days` or `custom`.
|
||||
Future<void> legacyDeviceActivityMeterTimeRangeChanged(String range) =>
|
||||
trackEvent('${_prefix}_activity_meter_time_range_changed', {
|
||||
'range': range,
|
||||
});
|
||||
|
||||
// ─── Health ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// User changed the heart-rate sampling frequency.
|
||||
Future<void> legacyDeviceHealthHeartRateFrequencyChanged(int seconds) =>
|
||||
trackEvent('${_prefix}_health_heart_rate_frequency_changed', {
|
||||
@@ -76,4 +128,33 @@ mixin DeviceTracking on Tracking {
|
||||
/// User triggered an on-demand health measurement (heart rate / SpO2).
|
||||
Future<void> legacyDeviceHealthMeasurementStarted() =>
|
||||
trackEvent('${_prefix}_health_measurement_started');
|
||||
|
||||
/// User changed the time range filter on the health screen.
|
||||
Future<void> legacyDeviceHealthTimeRangeChanged(String range) =>
|
||||
trackEvent('${_prefix}_health_time_range_changed', {'range': range});
|
||||
|
||||
// ─── Apps usage ──────────────────────────────────────────────────────────
|
||||
|
||||
/// User changed the time range filter on the apps usage screen.
|
||||
/// [range] is `today`, `seven_days`, `thirty_days` or `custom`.
|
||||
/// [totalDurationSeconds] is the total time across all apps for that range.
|
||||
/// [topAppName] is the app with the highest duration in the result set
|
||||
/// (empty string if no data).
|
||||
Future<void> legacyDeviceAppsUseTimeRangeChanged({
|
||||
required String range,
|
||||
required int totalDurationSeconds,
|
||||
required String topAppName,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_apps_use_time_range_changed', {
|
||||
'range': range,
|
||||
'total_duration_seconds': totalDurationSeconds,
|
||||
'top_app_name': topAppName,
|
||||
});
|
||||
|
||||
// ─── Call history ────────────────────────────────────────────────────────
|
||||
|
||||
/// User changed the filter on the call history screen.
|
||||
/// [filter] is `all`, `incoming`, `outgoing` or `missed`.
|
||||
Future<void> legacyDeviceCallHistoryFilterChanged(String filter) =>
|
||||
trackEvent('${_prefix}_call_history_filter_changed', {'filter': filter});
|
||||
}
|
||||
|
||||
@@ -58,4 +58,127 @@ mixin LocationTracking on Tracking {
|
||||
trackEvent('${_prefix}_map_route_trail_toggled', {
|
||||
'visible': visible.toString(),
|
||||
});
|
||||
|
||||
// ─── Place creation funnel ───────────────────────────────────────────────
|
||||
|
||||
/// User tapped the "add geofence" or "add frequent place" button and
|
||||
/// entered placing mode. [mode] is `geofence` or `frequent_place`.
|
||||
Future<void> legacyLocationPlaceCreationStarted(String mode) =>
|
||||
trackEvent('${_prefix}_place_creation_started', {'mode': mode});
|
||||
|
||||
/// User confirmed the point on the map (tapped to drop the pin). For
|
||||
/// geofences, this leads to the radius-adjusting step. For frequent places,
|
||||
/// this triggers the actual creation flow.
|
||||
Future<void> legacyLocationPointConfirmed(String mode) =>
|
||||
trackEvent('${_prefix}_point_confirmed', {'mode': mode});
|
||||
|
||||
/// User confirmed the geofence radius (last step before API call).
|
||||
/// [isEditing] distinguishes editing an existing geofence from creating a
|
||||
/// new one.
|
||||
Future<void> legacyLocationRadiusConfirmed({
|
||||
required double radius,
|
||||
required bool isEditing,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_radius_confirmed', {
|
||||
'radius': radius,
|
||||
'is_editing': isEditing.toString(),
|
||||
});
|
||||
|
||||
/// User abandoned the creation or edit flow before completing it. [atStep]
|
||||
/// is `picking_point` or `adjusting_radius`. [mode] is the mode that was
|
||||
/// active when cancelled.
|
||||
Future<void> legacyLocationPlaceCreationCancelled({
|
||||
required String mode,
|
||||
required String atStep,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_place_creation_cancelled', {
|
||||
'mode': mode,
|
||||
'at_step': atStep,
|
||||
});
|
||||
|
||||
// ─── Exploration / edit entry points ─────────────────────────────────────
|
||||
|
||||
/// User tapped a geofence on the map to inspect it.
|
||||
Future<void> legacyLocationGeofenceSelected() =>
|
||||
trackEvent('${_prefix}_geofence_selected');
|
||||
|
||||
/// User tapped "edit" on a selected geofence.
|
||||
Future<void> legacyLocationGeofenceEditStarted() =>
|
||||
trackEvent('${_prefix}_geofence_edit_started');
|
||||
|
||||
/// User tapped a frequent place on the map to inspect it.
|
||||
Future<void> legacyLocationFrequentPlaceSelected() =>
|
||||
trackEvent('${_prefix}_frequent_place_selected');
|
||||
|
||||
/// User tapped a position pin from the route history to see its details.
|
||||
Future<void> legacyLocationHistoryPositionSelected() =>
|
||||
trackEvent('${_prefix}_history_position_selected');
|
||||
|
||||
/// User cleared the route history trail from the map.
|
||||
Future<void> legacyLocationHistoryCleared() =>
|
||||
trackEvent('${_prefix}_history_cleared');
|
||||
|
||||
// ─── Dismissals (complete the exploration funnels) ───────────────────────
|
||||
|
||||
Future<void> legacyLocationGeofenceDismissed() =>
|
||||
trackEvent('${_prefix}_geofence_dismissed');
|
||||
|
||||
Future<void> legacyLocationFrequentPlaceDismissed() =>
|
||||
trackEvent('${_prefix}_frequent_place_dismissed');
|
||||
|
||||
Future<void> legacyLocationHistoryPositionDismissed() =>
|
||||
trackEvent('${_prefix}_history_position_dismissed');
|
||||
|
||||
// ─── Live tracking + UI engagement ───────────────────────────────────────
|
||||
|
||||
/// User toggled "follow device" mode on the map.
|
||||
Future<void> legacyLocationFollowingToggled(bool enabled) =>
|
||||
trackEvent('${_prefix}_following_toggled', {
|
||||
'enabled': enabled.toString(),
|
||||
});
|
||||
|
||||
/// User expanded or collapsed the actions drawer on the map.
|
||||
Future<void> legacyLocationMapActionsExpanded(bool expanded) =>
|
||||
trackEvent('${_prefix}_map_actions_expanded', {
|
||||
'expanded': expanded.toString(),
|
||||
});
|
||||
|
||||
// ─── Map zoom (debounced at the view model to avoid flooding) ────────────
|
||||
|
||||
/// User finished a zoom interaction. The view model debounces rapid zoom
|
||||
/// changes (e.g. continuous pinch gestures) so this fires once the zoom has
|
||||
/// settled, not on every delta.
|
||||
Future<void> legacyLocationMapZoomed(double zoom) =>
|
||||
trackEvent('${_prefix}_map_zoomed', {'zoom': zoom});
|
||||
|
||||
// ─── Map style + top-level map actions ──────────────────────────────────
|
||||
|
||||
/// User changed the visual style of the map. [style] is one of
|
||||
/// `standard`, `voyager`, `light`, `dark`, `satellite`.
|
||||
Future<void> legacyLocationMapStyleChanged(String style) =>
|
||||
trackEvent('${_prefix}_map_style_changed', {'style': style});
|
||||
|
||||
/// ⭐ User tapped "share location" — typically sends the kid's position
|
||||
/// via the native share sheet to a messaging app. This is one of the most
|
||||
/// viral actions of the product.
|
||||
Future<void> legacyLocationShared() => trackEvent('${_prefix}_shared');
|
||||
|
||||
/// User tapped the "center on device" / my-location button on the map.
|
||||
Future<void> legacyLocationMapCenterTapped() =>
|
||||
trackEvent('${_prefix}_map_center_tapped');
|
||||
|
||||
/// User tapped the refresh button on the map (different from the pull-to-
|
||||
/// refresh in the control panel — this is the in-map refresh action).
|
||||
Future<void> legacyLocationMapRefreshTapped() =>
|
||||
trackEvent('${_prefix}_map_refresh_tapped');
|
||||
|
||||
/// User opened the bottom list sheet containing geofences, frequent
|
||||
/// places and position history.
|
||||
Future<void> legacyLocationListSheetOpened() =>
|
||||
trackEvent('${_prefix}_list_sheet_opened');
|
||||
|
||||
/// User filtered the position-history list by type. [type] is the raw
|
||||
/// position type string (e.g. `gps`, `wifi`, `sos`) or `all` when cleared.
|
||||
Future<void> legacyLocationHistoryTypeFilterChanged(String type) =>
|
||||
trackEvent('${_prefix}_history_type_filter_changed', {'type': type});
|
||||
}
|
||||
|
||||
@@ -3,87 +3,50 @@ import 'package:sf_tracking/src/tracking.dart';
|
||||
const _prefix = 'legacy_settings';
|
||||
|
||||
/// Tracking events for the legacy settings module (alarms, SOS, language,
|
||||
/// alerts, timezone, wifi, etc). Mixed into `SfTrackingRepository`.
|
||||
/// alerts, timezone, wifi, remote management, etc). Mixed into
|
||||
/// `SfTrackingRepository`.
|
||||
mixin SettingsTracking on Tracking {
|
||||
// ─── Alarms ──────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsAlarmAdded() =>
|
||||
trackEvent('${_prefix}_alarm_added');
|
||||
/// User created a new alarm on the device. [time] is `HH:mm`.
|
||||
Future<void> legacySettingsAlarmAdded({required String time}) =>
|
||||
trackEvent('${_prefix}_alarm_added', {'time': time});
|
||||
|
||||
/// User edited an existing alarm. [time] is the new `HH:mm`.
|
||||
Future<void> legacySettingsAlarmUpdated({required String time}) =>
|
||||
trackEvent('${_prefix}_alarm_updated', {'time': time});
|
||||
|
||||
Future<void> legacySettingsAlarmRemoved() =>
|
||||
trackEvent('${_prefix}_alarm_removed');
|
||||
|
||||
// ─── SOS contacts ────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsSosContactAdded() =>
|
||||
trackEvent('${_prefix}_sos_contact_added');
|
||||
Future<void> legacySettingsSosContactAdded({required int totalCount}) =>
|
||||
trackEvent('${_prefix}_sos_contact_added', {'total_count': totalCount});
|
||||
|
||||
Future<void> legacySettingsSosContactRemoved() =>
|
||||
trackEvent('${_prefix}_sos_contact_removed');
|
||||
Future<void> legacySettingsSosContactRemoved({required int totalCount}) =>
|
||||
trackEvent('${_prefix}_sos_contact_removed', {'total_count': totalCount});
|
||||
|
||||
// ─── Block phone whitelist ───────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsBlockPhoneContactAdded() =>
|
||||
trackEvent('${_prefix}_block_phone_contact_added');
|
||||
Future<void> legacySettingsBlockPhoneContactAdded({required int totalCount}) =>
|
||||
trackEvent('${_prefix}_block_phone_contact_added', {
|
||||
'total_count': totalCount,
|
||||
});
|
||||
|
||||
Future<void> legacySettingsBlockPhoneContactRemoved() =>
|
||||
trackEvent('${_prefix}_block_phone_contact_removed');
|
||||
Future<void> legacySettingsBlockPhoneContactRemoved({
|
||||
required int totalCount,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_block_phone_contact_removed', {
|
||||
'total_count': totalCount,
|
||||
});
|
||||
|
||||
// ─── Disable functions (parental controls) ───────────────────────────────
|
||||
|
||||
/// User toggled a function (e.g. `keyboard`, `gps`).
|
||||
/// Legacy aggregate event: user saved the disable functions screen.
|
||||
Future<void> legacySettingsDisableFunctionsChanged() =>
|
||||
trackEvent('${_prefix}_disable_functions_changed');
|
||||
|
||||
// ─── Language ────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsLanguageChanged(String language) =>
|
||||
trackEvent('${_prefix}_language_changed', {'language': language});
|
||||
|
||||
// ─── Alerts ──────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsAlertsConfigured() =>
|
||||
trackEvent('${_prefix}_alerts_configured');
|
||||
|
||||
// ─── Timezone ────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsTimezoneChanged(String timezone) =>
|
||||
trackEvent('${_prefix}_timezone_changed', {'timezone': timezone});
|
||||
|
||||
// ─── WiFi ────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsWifiAdded() =>
|
||||
trackEvent('${_prefix}_wifi_added');
|
||||
|
||||
Future<void> legacySettingsWifiRemoved() =>
|
||||
trackEvent('${_prefix}_wifi_removed');
|
||||
|
||||
// ─── Sound ───────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsSoundChanged() =>
|
||||
trackEvent('${_prefix}_sound_changed');
|
||||
|
||||
// ─── Sync clock ──────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsSyncClockTriggered() =>
|
||||
trackEvent('${_prefix}_sync_clock_triggered');
|
||||
|
||||
// ─── Alarms (extra) ──────────────────────────────────────────────────────
|
||||
|
||||
/// User edited an existing alarm.
|
||||
Future<void> legacySettingsAlarmUpdated() =>
|
||||
trackEvent('${_prefix}_alarm_updated');
|
||||
|
||||
// ─── Battery ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// User toggled night mode (battery saving) on the device.
|
||||
Future<void> legacySettingsBatteryNightModeToggled(bool enabled) =>
|
||||
trackEvent('${_prefix}_battery_night_mode_toggled', {
|
||||
'enabled': enabled.toString(),
|
||||
});
|
||||
|
||||
// ─── Disable functions (granular) ────────────────────────────────────────
|
||||
|
||||
/// Granular: user toggled the device keyboard off/on.
|
||||
Future<void> legacySettingsDisableFunctionsKeyboardToggled(bool enabled) =>
|
||||
trackEvent('${_prefix}_disable_functions_keyboard_toggled', {
|
||||
@@ -95,4 +58,71 @@ mixin SettingsTracking on Tracking {
|
||||
trackEvent('${_prefix}_disable_functions_gps_toggled', {
|
||||
'enabled': enabled.toString(),
|
||||
});
|
||||
|
||||
// ─── Language ────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsLanguageChanged(String language) =>
|
||||
trackEvent('${_prefix}_language_changed', {'language': language});
|
||||
|
||||
// ─── Alerts ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// User saved the alerts configuration. [alertCount] is how many alerts
|
||||
/// are currently enabled. [alertsEnabled] is the comma-joined list of the
|
||||
/// active alert type names, truncated to a safe length.
|
||||
Future<void> legacySettingsAlertsConfigured({
|
||||
required int alertCount,
|
||||
required String alertsEnabled,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_alerts_configured', {
|
||||
'alert_count': alertCount,
|
||||
'alerts_enabled': alertsEnabled,
|
||||
});
|
||||
|
||||
// ─── Timezone ────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsTimezoneChanged(String timezone) =>
|
||||
trackEvent('${_prefix}_timezone_changed', {'timezone': timezone});
|
||||
|
||||
// ─── WiFi ────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsWifiAdded({required int totalCount}) =>
|
||||
trackEvent('${_prefix}_wifi_added', {'total_count': totalCount});
|
||||
|
||||
Future<void> legacySettingsWifiRemoved({required int totalCount}) =>
|
||||
trackEvent('${_prefix}_wifi_removed', {'total_count': totalCount});
|
||||
|
||||
// ─── Sound ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// User changed the sound mode of the device. [mode] is typically
|
||||
/// `normal`, `silent` or `vibrate`.
|
||||
Future<void> legacySettingsSoundChanged({required String mode}) =>
|
||||
trackEvent('${_prefix}_sound_changed', {'mode': mode});
|
||||
|
||||
// ─── Sync clock ──────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> legacySettingsSyncClockTriggered() =>
|
||||
trackEvent('${_prefix}_sync_clock_triggered');
|
||||
|
||||
// ─── Battery ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// User toggled night mode (battery saving) on the device.
|
||||
Future<void> legacySettingsBatteryNightModeToggled(bool enabled) =>
|
||||
trackEvent('${_prefix}_battery_night_mode_toggled', {
|
||||
'enabled': enabled.toString(),
|
||||
});
|
||||
|
||||
// ─── Remote management (destructive commands) ───────────────────────────
|
||||
|
||||
/// User triggered a remote shutdown of the device.
|
||||
Future<void> legacySettingsRemoteManagementShutdown() =>
|
||||
trackEvent('${_prefix}_remote_management_shutdown');
|
||||
|
||||
/// User triggered a remote restart of the device.
|
||||
Future<void> legacySettingsRemoteManagementRestart() =>
|
||||
trackEvent('${_prefix}_remote_management_restart');
|
||||
|
||||
/// ⚠️ CHURN SIGNAL — user triggered a factory reset of the device.
|
||||
/// This effectively wipes the device and often precedes unlinking.
|
||||
Future<void> legacySettingsRemoteManagementFactoryReset() =>
|
||||
trackEvent('${_prefix}_remote_management_factory_reset');
|
||||
}
|
||||
|
||||
@@ -6,9 +6,17 @@ const _prefix = 'legacy_support';
|
||||
/// `SfTrackingRepository`.
|
||||
mixin SupportTracking on Tracking {
|
||||
/// User tapped a button to contact support. [channel] is `email`, `phone`,
|
||||
/// `whatsapp`, etc — depends on the support screen UX. Note that this only
|
||||
/// tracks the *intent* (the button tap that opens the external client). We
|
||||
/// can't confirm the message was actually sent.
|
||||
Future<void> legacySupportContactInitiated(String channel) =>
|
||||
trackEvent('${_prefix}_contact_initiated', {'channel': channel});
|
||||
/// `whatsapp`, etc — depends on the support screen UX. [country] is the
|
||||
/// country the user picked in the support form (may be empty if not
|
||||
/// selected). Note that this only tracks the *intent* (the button tap
|
||||
/// that opens the external client). We can't confirm the message was
|
||||
/// actually sent.
|
||||
Future<void> legacySupportContactInitiated({
|
||||
required String channel,
|
||||
required String country,
|
||||
}) =>
|
||||
trackEvent('${_prefix}_contact_initiated', {
|
||||
'channel': channel,
|
||||
'country': country,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user