From 2932ee7d4cff4bb077158e57ac8abd5ead664e9b Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Sat, 6 Sep 2025 06:52:21 +0530 Subject: [PATCH] Legacy package --- mobile/packages/legacy/analysis_options.yaml | 1 + mobile/packages/legacy/lib/ente_legacy.dart | 5 + .../legacy/lib/models/emergency_models.dart | 153 ++ .../legacy/lib/pages/emergency_page.dart | 556 +++++++ .../legacy/lib/pages/other_contact_page.dart | 287 ++++ .../lib/pages/recover_others_account.dart | 362 +++++ .../legacy/lib/pages/select_contact_page.dart | 334 ++++ .../lib/services/emergency_service.dart | 298 ++++ mobile/packages/legacy/pubspec.lock | 1366 +++++++++++++++++ mobile/packages/legacy/pubspec.yaml | 49 + mobile/packages/legacy/pubspec_overrides.yaml | 24 + .../lib/l10n/strings_localizations.dart | 12 + 12 files changed, 3447 insertions(+) create mode 100644 mobile/packages/legacy/analysis_options.yaml create mode 100644 mobile/packages/legacy/lib/ente_legacy.dart create mode 100644 mobile/packages/legacy/lib/models/emergency_models.dart create mode 100644 mobile/packages/legacy/lib/pages/emergency_page.dart create mode 100644 mobile/packages/legacy/lib/pages/other_contact_page.dart create mode 100644 mobile/packages/legacy/lib/pages/recover_others_account.dart create mode 100644 mobile/packages/legacy/lib/pages/select_contact_page.dart create mode 100644 mobile/packages/legacy/lib/services/emergency_service.dart create mode 100644 mobile/packages/legacy/pubspec.lock create mode 100644 mobile/packages/legacy/pubspec.yaml create mode 100644 mobile/packages/legacy/pubspec_overrides.yaml diff --git a/mobile/packages/legacy/analysis_options.yaml b/mobile/packages/legacy/analysis_options.yaml new file mode 100644 index 0000000000..f04c6cf0f3 --- /dev/null +++ b/mobile/packages/legacy/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/mobile/packages/legacy/lib/ente_legacy.dart b/mobile/packages/legacy/lib/ente_legacy.dart new file mode 100644 index 0000000000..2a47e2705c --- /dev/null +++ b/mobile/packages/legacy/lib/ente_legacy.dart @@ -0,0 +1,5 @@ +export 'models/emergency_models.dart'; +export 'pages/emergency_page.dart'; +export 'pages/other_contact_page.dart'; +export 'pages/recover_others_account.dart'; +export 'services/emergency_service.dart'; \ No newline at end of file diff --git a/mobile/packages/legacy/lib/models/emergency_models.dart b/mobile/packages/legacy/lib/models/emergency_models.dart new file mode 100644 index 0000000000..2017a3639b --- /dev/null +++ b/mobile/packages/legacy/lib/models/emergency_models.dart @@ -0,0 +1,153 @@ +import "package:ente_sharing/models/user.dart"; + +enum ContactState { + userInvitedContact, + userRevokedContact, + contactAccepted, + contactLeft, + contactDenied, + unknown, +} + +extension ContactStateExtension on ContactState { + String get stringValue { + switch (this) { + case ContactState.userInvitedContact: + return "INVITED"; + case ContactState.userRevokedContact: + return "REVOKED"; + case ContactState.contactAccepted: + return "ACCEPTED"; + case ContactState.contactLeft: + return "CONTACT_LEFT"; + case ContactState.contactDenied: + return "CONTACT_DENIED"; + default: + return "UNKNOWN"; + } + } + + static ContactState fromString(String value) { + switch (value) { + case "INVITED": + return ContactState.userInvitedContact; + case "REVOKED": + return ContactState.userRevokedContact; + case "ACCEPTED": + return ContactState.contactAccepted; + case "CONTACT_LEFT": + return ContactState.contactLeft; + case "CONTACT_DENIED": + return ContactState.contactDenied; + default: + return ContactState.unknown; + } + } +} + +class EmergencyContact { + final User user; + final User emergencyContact; + final ContactState state; + final int recoveryNoticeInDays; + + EmergencyContact( + this.user, + this.emergencyContact, + this.state, + this.recoveryNoticeInDays, + ); + + // copyWith + EmergencyContact copyWith({ + User? user, + User? emergencyContact, + ContactState? state, + int? recoveryNoticeInDays, + }) { + return EmergencyContact( + user ?? this.user, + emergencyContact ?? this.emergencyContact, + state ?? this.state, + recoveryNoticeInDays ?? this.recoveryNoticeInDays, + ); + } + + // fromJson + EmergencyContact.fromJson(Map json) + : user = User.fromMap(json['user']), + emergencyContact = User.fromMap(json['emergencyContact']), + state = ContactStateExtension.fromString(json['state'] as String), + recoveryNoticeInDays = json['recoveryNoticeInDays']; + + bool isCurrentUserContact(int userID) { + return user.id == userID; + } + + bool isPendingInvite() { + return state == ContactState.userInvitedContact; + } +} + +class EmergencyInfo { + // List of emergency contacts added by the user + final List contacts; + + // List of recovery sessions that are created to recover current user account + final List recoverSessions; + + // List of emergency contacts that have added current user as their emergency contact + final List othersEmergencyContact; + + // List of recovery sessions that are created to recover grantor's account + final List othersRecoverySession; + + EmergencyInfo( + this.contacts, + this.recoverSessions, + this.othersEmergencyContact, + this.othersRecoverySession, + ); + + // from json + EmergencyInfo.fromJson(Map json) + : contacts = (json['contacts'] as List) + .map((contact) => EmergencyContact.fromJson(contact)) + .toList(), + recoverSessions = (json['recoverSessions'] as List) + .map((session) => RecoverySessions.fromJson(session)) + .toList(), + othersEmergencyContact = (json['othersEmergencyContact'] as List) + .map((grantor) => EmergencyContact.fromJson(grantor)) + .toList(), + othersRecoverySession = (json['othersRecoverySession'] as List) + .map((session) => RecoverySessions.fromJson(session)) + .toList(); +} + +class RecoverySessions { + final String id; + final User user; + final User emergencyContact; + final String status; + final int waitTill; + final int createdAt; + + RecoverySessions( + this.id, + this.user, + this.emergencyContact, + this.status, + this.waitTill, + this.createdAt, + ); + + // fromJson + RecoverySessions.fromJson(Map json) + : id = json['id'], + user = User.fromMap(json['user']), + emergencyContact = User.fromMap(json['emergencyContact']), + status = json['status'], + waitTill = json['waitTill'], + createdAt = json['createdAt']; +} diff --git a/mobile/packages/legacy/lib/pages/emergency_page.dart b/mobile/packages/legacy/lib/pages/emergency_page.dart new file mode 100644 index 0000000000..311f600264 --- /dev/null +++ b/mobile/packages/legacy/lib/pages/emergency_page.dart @@ -0,0 +1,556 @@ +import "dart:async"; + +import "package:ente_configuration/base_configuration.dart"; +import "package:ente_legacy/models/emergency_models.dart"; +import "package:ente_legacy/pages/other_contact_page.dart"; +import "package:ente_legacy/pages/select_contact_page.dart"; +import "package:ente_legacy/services/emergency_service.dart"; +import "package:ente_sharing/user_avator_widget.dart"; +import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/components/action_sheet_widget.dart"; +import "package:ente_ui/components/buttons/button_widget.dart"; +import "package:ente_ui/components/buttons/models/button_type.dart"; +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/divider_widget.dart"; +import "package:ente_ui/components/loading_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/components/menu_section_title.dart"; +import "package:ente_ui/components/notification_widget.dart"; +import "package:ente_ui/components/title_bar_title_widget.dart"; +import "package:ente_ui/components/title_bar_widget.dart"; +import "package:ente_ui/theme/colors.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_ui/utils/toast_util.dart"; +import "package:ente_utils/navigation_util.dart"; +import "package:flutter/foundation.dart"; +import 'package:flutter/material.dart'; +import "package:flutter_svg/svg.dart"; + +class EmergencyPage extends StatefulWidget { + final BaseConfiguration config; + + const EmergencyPage({ + required this.config, + super.key, + }); + + @override + State createState() => _EmergencyPageState(); +} + +class _EmergencyPageState extends State { + late int currentUserID; + EmergencyInfo? info; + bool hasTrustedContact = false; + + @override + void initState() { + super.initState(); + currentUserID = widget.config.getUserID()!; + Future.delayed( + const Duration(seconds: 0), + () async { + unawaited(_fetchData()); + }, + ); + } + + Future _fetchData() async { + try { + final result = await EmergencyContactService.instance.getInfo(); + if (mounted) { + setState(() { + info = result; + if (info != null) { + hasTrustedContact = info!.contacts.isNotEmpty; + } + }); + } + } catch (e) { + showShortToast( + context, + context.strings.somethingWentWrong, + ); + } + } + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + final List othersTrustedContacts = + info?.othersEmergencyContact ?? []; + final List trustedContacts = info?.contacts ?? []; + + return Scaffold( + body: CustomScrollView( + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: context.strings.legacy, + ), + ), + if (info == null) + const SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: EnteLoadingWidget(), + ), + ), + if (info != null && info!.recoverSessions.isNotEmpty) + SliverPadding( + padding: const EdgeInsets.only( + top: 20, + left: 16, + right: 16, + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: NotificationWidget( + startIcon: Icons.warning_amber_rounded, + text: context.strings.recoveryWarning, + actionIcon: null, + onTap: () {}, + ), + ); + } + final RecoverySessions recoverSession = + info!.recoverSessions[index - 1]; + return MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: recoverSession.emergencyContact.email, + makeTextBold: recoverSession.status.isNotEmpty, + textColor: colorScheme.warning500, + ), + leadingIconWidget: UserAvatarWidget( + recoverSession.emergencyContact, + currentUserID: currentUserID, + config: widget.config, + ), + leadingIconSize: 24, + menuItemColor: colorScheme.fillFaint, + singleBorderRadius: 8, + trailingIcon: Icons.chevron_right, + onTap: () async { + await showRejectRecoveryDialog(recoverSession); + }, + ); + }, + childCount: 1 + info!.recoverSessions.length, + ), + ), + ), + if (info != null) + SliverPadding( + padding: const EdgeInsets.only( + top: 16, + left: 16, + right: 16, + bottom: 8, + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == 0 && trustedContacts.isNotEmpty) { + return MenuSectionTitle( + title: context.strings.trustedContacts, + ); + } else if (index > 0 && index <= trustedContacts.length) { + final listIndex = index - 1; + final contact = trustedContacts[listIndex]; + return Column( + children: [ + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: contact.emergencyContact.email, + subTitle: contact.isPendingInvite() ? "⚠" : null, + makeTextBold: contact.isPendingInvite(), + ), + leadingIconSize: 24.0, + surfaceExecutionStates: false, + alwaysShowSuccessState: false, + leadingIconWidget: UserAvatarWidget( + contact.emergencyContact, + type: AvatarType.mini, + currentUserID: currentUserID, + config: widget.config, + ), + menuItemColor: + getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right, + trailingIconIsMuted: true, + onTap: () async { + await showRevokeOrRemoveDialog(context, contact); + }, + isTopBorderRadiusRemoved: listIndex > 0, + isBottomBorderRadiusRemoved: true, + singleBorderRadius: 8, + ), + DividerWidget( + dividerType: DividerType.menu, + bgColor: getEnteColorScheme(context).fillFaint, + ), + ], + ); + } else if (index == (1 + trustedContacts.length)) { + if (trustedContacts.isEmpty) { + return Column( + children: [ + const SizedBox(height: 20), + Text( + context.strings.legacyPageDesc, + style: getEnteTextTheme(context).body, + ), + SizedBox( + height: 200, + width: 200, + child: SvgPicture.asset( + getEnteColorScheme(context).backdropBase == + backgroundBaseDark + ? "assets/icons/legacy-light.svg" + : "assets/icons/legacy-dark.svg", + width: 156, + height: 152, + ), + ), + Text( + context.strings.legacyPageDesc2, + style: getEnteTextTheme(context).smallMuted, + ), + const SizedBox(height: 16), + ButtonWidget( + buttonType: ButtonType.primary, + labelText: context.strings.addTrustedContact, + shouldSurfaceExecutionStates: false, + onTap: () async { + await routeToPage( + context, + AddContactPage( + info!, + config: widget.config, + ), + forceCustomPageRoute: true, + ); + unawaited(_fetchData()); + }, + ), + ], + ); + } + return MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: trustedContacts.isNotEmpty + ? context.strings.addMore + : context.strings.addTrustedContact, + makeTextBold: true, + ), + leadingIcon: Icons.add_outlined, + surfaceExecutionStates: false, + menuItemColor: getEnteColorScheme(context).fillFaint, + onTap: () async { + await routeToPage( + context, + AddContactPage( + info!, + config: widget.config, + ), + forceCustomPageRoute: true, + ); + unawaited(_fetchData()); + }, + isTopBorderRadiusRemoved: trustedContacts.isNotEmpty, + singleBorderRadius: 8, + ); + } + return const SizedBox.shrink(); + }, + childCount: 1 + trustedContacts.length + 1, + ), + ), + ), + if (info != null && info!.othersEmergencyContact.isNotEmpty) + SliverPadding( + padding: const EdgeInsets.only(top: 0, left: 16, right: 16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == 0 && (othersTrustedContacts.isNotEmpty)) { + return Column( + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: DividerWidget( + dividerType: DividerType.solid, + ), + ), + MenuSectionTitle( + title: context.strings.legacyAccounts, + ), + ], + ); + } else if (index > 0 && + index <= othersTrustedContacts.length) { + final listIndex = index - 1; + final currentUser = othersTrustedContacts[listIndex]; + final isLastItem = index == othersTrustedContacts.length; + return Column( + children: [ + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: currentUser.user.email, + makeTextBold: currentUser.isPendingInvite(), + subTitle: + currentUser.isPendingInvite() ? "⚠" : null, + ), + leadingIconSize: 24.0, + leadingIconWidget: UserAvatarWidget( + currentUser.user, + type: AvatarType.mini, + currentUserID: currentUserID, + config: widget.config, + ), + menuItemColor: + getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right, + trailingIconIsMuted: true, + onTap: () async { + if (currentUser.isPendingInvite()) { + await showAcceptOrDeclineDialog( + context, + currentUser, + ); + } else { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return OtherContactPage( + contact: currentUser, + emergencyInfo: info!, + config: widget.config, + ); + }, + ), + ); + if (mounted) { + unawaited(_fetchData()); + } + } + }, + isTopBorderRadiusRemoved: listIndex > 0, + isBottomBorderRadiusRemoved: !isLastItem, + singleBorderRadius: 8, + surfaceExecutionStates: false, + ), + isLastItem + ? const SizedBox.shrink() + : DividerWidget( + dividerType: DividerType.menu, + bgColor: + getEnteColorScheme(context).fillFaint, + ), + ], + ); + } + return const SizedBox.shrink(); + }, + childCount: 1 + othersTrustedContacts.length + 1, + ), + ), + ), + ], + ), + ); + } + + Future showRevokeOrRemoveDialog( + BuildContext context, + EmergencyContact contact, + ) async { + if (contact.isPendingInvite()) { + await showActionSheet( + context: context, + body: + "You have invited ${contact.emergencyContact.email} to be a trusted contact", + bodyHighlight: "They are yet to accept your invite", + buttons: [ + ButtonWidget( + labelText: "context.strings.removeInvite", + buttonType: ButtonType.critical, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.first, + shouldStickToDarkTheme: true, + shouldSurfaceExecutionStates: true, + shouldShowSuccessConfirmation: false, + onTap: () async { + await EmergencyContactService.instance + .updateContact(contact, ContactState.userRevokedContact); + info?.contacts.remove(contact); + if (mounted) { + setState(() {}); + unawaited(_fetchData()); + } + }, + isInAlert: true, + ), + ButtonWidget( + labelText: context.strings.cancel, + buttonType: ButtonType.tertiary, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.second, + shouldStickToDarkTheme: true, + isInAlert: true, + ), + ], + ); + } else { + await showActionSheet( + context: context, + body: + "You have added ${contact.emergencyContact.email} as a trusted contact", + bodyHighlight: "They have accepted your invite", + buttons: [ + ButtonWidget( + labelText: "context.strings.remove", + buttonType: ButtonType.critical, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.second, + shouldStickToDarkTheme: true, + shouldSurfaceExecutionStates: true, + shouldShowSuccessConfirmation: false, + onTap: () async { + await EmergencyContactService.instance + .updateContact(contact, ContactState.userRevokedContact); + info?.contacts.remove(contact); + if (mounted) { + setState(() {}); + unawaited(_fetchData()); + } + }, + isInAlert: true, + ), + ButtonWidget( + labelText: context.strings.cancel, + buttonType: ButtonType.tertiary, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.third, + shouldStickToDarkTheme: true, + isInAlert: true, + ), + ], + ); + } + } + + Future showAcceptOrDeclineDialog( + BuildContext context, + EmergencyContact contact, + ) async { + await showActionSheet( + context: context, + buttons: [ + ButtonWidget( + labelText: "context.strings.acceptTrustInvite", + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.first, + onTap: () async { + await EmergencyContactService.instance + .updateContact(contact, ContactState.contactAccepted); + final updatedContact = + contact.copyWith(state: ContactState.contactAccepted); + info?.othersEmergencyContact.remove(contact); + info?.othersEmergencyContact.add(updatedContact); + if (mounted) { + setState(() {}); + } + }, + isInAlert: true, + ), + ButtonWidget( + labelText: context.strings.declineTrustInvite, + buttonType: ButtonType.critical, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.second, + shouldStickToDarkTheme: true, + onTap: () async { + await EmergencyContactService.instance + .updateContact(contact, ContactState.contactDenied); + info?.othersEmergencyContact.remove(contact); + if (mounted) { + setState(() {}); + } + }, + isInAlert: true, + ), + ButtonWidget( + labelText: context.strings.cancel, + buttonType: ButtonType.tertiary, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.third, + shouldStickToDarkTheme: true, + isInAlert: true, + ), + ], + body: "context.strings.legacyInvite(email: contact.user.email)", + actionSheetType: ActionSheetType.defaultActionSheet, + ); + return; + } + + Future showRejectRecoveryDialog(RecoverySessions session) async { + final String emergencyContactEmail = session.emergencyContact.email; + await showActionSheet( + context: context, + buttons: [ + ButtonWidget( + labelText: context.strings.rejectRecovery, + buttonSize: ButtonSize.large, + shouldStickToDarkTheme: true, + buttonType: ButtonType.critical, + buttonAction: ButtonAction.first, + onTap: () async { + await EmergencyContactService.instance.rejectRecovery(session); + info?.recoverSessions + .removeWhere((element) => element.id == session.id); + if (mounted) { + setState(() {}); + } + unawaited(_fetchData()); + }, + isInAlert: true, + ), + if (kDebugMode) + ButtonWidget( + labelText: "Approve recovery (to be removed)", + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.second, + shouldStickToDarkTheme: true, + onTap: () async { + await EmergencyContactService.instance.approveRecovery(session); + if (mounted) { + setState(() {}); + } + unawaited(_fetchData()); + }, + isInAlert: true, + ), + ButtonWidget( + labelText: context.strings.cancel, + buttonType: ButtonType.tertiary, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.third, + shouldStickToDarkTheme: true, + isInAlert: true, + ), + ], + body: context.strings.recoveryWarningBody(emergencyContactEmail), + actionSheetType: ActionSheetType.defaultActionSheet, + ); + return; + } +} diff --git a/mobile/packages/legacy/lib/pages/other_contact_page.dart b/mobile/packages/legacy/lib/pages/other_contact_page.dart new file mode 100644 index 0000000000..d1830b41cd --- /dev/null +++ b/mobile/packages/legacy/lib/pages/other_contact_page.dart @@ -0,0 +1,287 @@ +import "dart:async"; + +import "package:collection/collection.dart"; +import "package:ente_base/models/key_attributes.dart"; +import "package:ente_configuration/base_configuration.dart"; +import "package:ente_legacy/models/emergency_models.dart"; +import "package:ente_legacy/pages/recover_others_account.dart"; +import "package:ente_legacy/services/emergency_service.dart"; +import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/components/action_sheet_widget.dart"; +import "package:ente_ui/components/buttons/button_widget.dart"; +import "package:ente_ui/components/buttons/models/button_type.dart"; +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/components/menu_section_title.dart"; +import "package:ente_ui/components/title_bar_title_widget.dart"; +import "package:ente_ui/theme/colors.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_ui/utils/dialog_util.dart"; +import "package:ente_utils/navigation_util.dart"; +import "package:flutter/material.dart"; +import "package:intl/intl.dart"; +import "package:logging/logging.dart"; + +// OtherContactPage is used to start recovery process for other user's account +// Based on the state of the contact & recovery session, it will show +// different UI +class OtherContactPage extends StatefulWidget { + final EmergencyContact contact; + final EmergencyInfo emergencyInfo; + final BaseConfiguration config; + + const OtherContactPage({ + required this.contact, + required this.emergencyInfo, + required this.config, + super.key, + }); + + @override + State createState() => _OtherContactPageState(); +} + +class _OtherContactPageState extends State { + late String accountEmail = widget.contact.user.email; + RecoverySessions? recoverySession; + String? waitTill; + final Logger _logger = Logger("_OtherContactPageState"); + late EmergencyInfo emergencyInfo = widget.emergencyInfo; + + @override + void initState() { + super.initState(); + recoverySession = widget.emergencyInfo.othersRecoverySession + .firstWhereOrNull((session) => session.user.email == accountEmail); + _fetchData(); + } + + Future _fetchData() async { + try { + final result = await EmergencyContactService.instance.getInfo(); + if (mounted) { + setState(() { + recoverySession = result.othersRecoverySession.firstWhereOrNull( + (session) => session.user.email == accountEmail, + ); + }); + } + } catch (e) { + _logger.severe("Error fetching data", e); + } + } + + @override + Widget build(BuildContext context) { + _logger.info('session ${widget.emergencyInfo}'); + if (recoverySession != null) { + final dateTime = DateTime.now().add( + Duration( + microseconds: recoverySession!.waitTill, + ), + ); + waitTill = _getFormattedTime(context, dateTime); + } + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + return Scaffold( + appBar: AppBar(), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + TitleBarTitleWidget( + title: context.strings.recoverAccount, + ), + Text( + accountEmail, + textAlign: TextAlign.left, + style: + textTheme.small.copyWith(color: colorScheme.textMuted), + ), + ], + ), + ), + const SizedBox(height: 12), + recoverySession == null + ? Text( + "You can recover $accountEmail's account in ${widget.contact.recoveryNoticeInDays} days" + " after starting the recovery process.", + style: textTheme.body, + ) + : (recoverySession!.status == "READY" + ? Text( + "Recovery ready for $accountEmail", + style: textTheme.body, + ) + : Text( + "You can recover $accountEmail's" + " account after $waitTill.", + style: textTheme.bodyBold, + )), + const SizedBox(height: 24), + if (recoverySession == null) + ButtonWidget( + // icon: Icons.start_outlined, + buttonType: ButtonType.trailingIconPrimary, + icon: Icons.start_outlined, + labelText: context.strings.startAccountRecoveryTitle, + onTap: widget.contact.isPendingInvite() + ? null + : () async { + final actionResult = await showChoiceActionSheet( + context, + title: context.strings.startAccountRecoveryTitle, + firstButtonLabel: context.strings.yes, + body: "Are you sure you want to initiate recovery?", + isCritical: true, + ); + if (actionResult?.action != null) { + if (actionResult!.action == ButtonAction.first) { + try { + await EmergencyContactService.instance + .startRecovery(widget.contact); + if (mounted) { + _fetchData().ignore(); + await showErrorDialog( + context, + context.strings.recoveryInitiated, + context.strings.recoveryInitiatedDesc( + widget.contact.recoveryNoticeInDays, + widget.config.getEmail()!, + ), + ); + } + } catch (e) { + showGenericErrorDialog(context: context, error: e) + .ignore(); + } + } + } + }, + ), + if (recoverySession != null && recoverySession!.status == "READY") + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: ButtonWidget( + buttonType: ButtonType.primary, + labelText: context.strings.recoverAccount, + onTap: () async { + try { + final (String key, KeyAttributes attributes) = + await EmergencyContactService.instance + .getRecoveryInfo(recoverySession!); + routeToPage( + context, + RecoverOthersAccount(key, attributes, recoverySession!), + ).ignore(); + } catch (e) { + showGenericErrorDialog(context: context, error: e) + .ignore(); + } + }, + ), + ), + if (recoverySession != null && + (recoverySession!.status == "WAITING" || + recoverySession!.status == "READY")) + ButtonWidget( + buttonType: ButtonType.neutral, + labelText: context.strings.cancelAccountRecovery, + shouldSurfaceExecutionStates: false, + onTap: () async { + final actionResult = await showChoiceActionSheet( + context, + title: context.strings.cancelAccountRecovery, + firstButtonLabel: context.strings.yes, + body: context.strings.cancelAccountRecoveryBody, + isCritical: true, + firstButtonOnTap: () async { + await EmergencyContactService.instance + .stopRecovery(recoverySession!); + }, + ); + if (actionResult?.action == ButtonAction.first) { + _fetchData().ignore(); + } + }, + ), + MenuSectionTitle( + title: context.strings.removeYourselfAsTrustedContact, + ), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.strings.remove, + textColor: warning500, + makeTextBold: true, + ), + leadingIcon: Icons.not_interested_outlined, + leadingIconColor: warning500, + menuItemColor: getEnteColorScheme(context).fillFaint, + surfaceExecutionStates: false, + onTap: () async { + await showRemoveSheet(); + }, + ), + ], + ), + ), + ); + } + + String _getFormattedTime(BuildContext context, DateTime dateTime) { + return DateFormat( + 'E, MMM d, y - HH:mm', + Localizations.localeOf(context).languageCode, + ).format( + dateTime, + ); + } + + Future showRemoveSheet() async { + await showActionSheet( + context: context, + buttons: [ + ButtonWidget( + labelText: context.strings.remove, + buttonSize: ButtonSize.large, + shouldStickToDarkTheme: true, + buttonType: ButtonType.critical, + buttonAction: ButtonAction.first, + onTap: () async { + try { + await EmergencyContactService.instance.updateContact( + widget.contact, + ContactState.contactLeft, + ); + Navigator.of(context).pop(); + } catch (e) { + showGenericErrorDialog(context: context, error: e).ignore(); + } + }, + isInAlert: true, + ), + ButtonWidget( + labelText: context.strings.cancel, + buttonType: ButtonType.tertiary, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.third, + shouldStickToDarkTheme: true, + isInAlert: true, + ), + ], + body: "Are you sure your want to stop being a trusted " + "contact for $accountEmail?", + title: context.strings.remove, + actionSheetType: ActionSheetType.defaultActionSheet, + ); + return; + } +} diff --git a/mobile/packages/legacy/lib/pages/recover_others_account.dart b/mobile/packages/legacy/lib/pages/recover_others_account.dart new file mode 100644 index 0000000000..6e847ab544 --- /dev/null +++ b/mobile/packages/legacy/lib/pages/recover_others_account.dart @@ -0,0 +1,362 @@ +import "dart:convert"; +import "dart:typed_data"; + +import "package:ente_accounts/models/set_keys_request.dart"; +import "package:ente_base/models/key_attributes.dart"; +import "package:ente_crypto_dart/ente_crypto_dart.dart"; +import "package:ente_legacy/models/emergency_models.dart"; +import "package:ente_legacy/services/emergency_service.dart"; +import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/components/buttons/dynamic_fab.dart"; +import "package:ente_ui/utils/dialog_util.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:logging/logging.dart"; +import "package:password_strength/password_strength.dart"; + +class RecoverOthersAccount extends StatefulWidget { + final String recoveryKey; + final KeyAttributes attributes; + final RecoverySessions sessions; + + const RecoverOthersAccount( + this.recoveryKey, + this.attributes, + this.sessions, { + super.key, + }); + + @override + State createState() => _RecoverOthersAccountState(); +} + +class _RecoverOthersAccountState extends State { + static const kMildPasswordStrengthThreshold = 0.4; + static const kStrongPasswordStrengthThreshold = 0.7; + + final _logger = Logger((_RecoverOthersAccountState).toString()); + final _passwordController1 = TextEditingController(), + _passwordController2 = TextEditingController(); + final Color _validFieldValueColor = const Color.fromRGBO(45, 194, 98, 0.2); + String _passwordInInputBox = ''; + String _passwordInInputConfirmationBox = ''; + double _passwordStrength = 0.0; + bool _password1Visible = false; + bool _password2Visible = false; + final _password1FocusNode = FocusNode(); + final _password2FocusNode = FocusNode(); + bool _password1InFocus = false; + bool _password2InFocus = false; + + bool _passwordsMatch = false; + bool _isPasswordValid = false; + + @override + void initState() { + super.initState(); + _password1FocusNode.addListener(() { + setState(() { + _password1InFocus = _password1FocusNode.hasFocus; + }); + }); + _password2FocusNode.addListener(() { + setState(() { + _password2InFocus = _password2FocusNode.hasFocus; + }); + }); + } + + @override + Widget build(BuildContext context) { + final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; + + FloatingActionButtonLocation? fabLocation() { + if (isKeypadOpen) { + return null; + } else { + return FloatingActionButtonLocation.centerFloat; + } + } + + String title = context.strings.setPasswordTitle; + title = context.strings.resetPasswordTitle; + return Scaffold( + resizeToAvoidBottomInset: isKeypadOpen, + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + color: Theme.of(context).iconTheme.color, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + elevation: 0, + ), + body: _getBody(title), + floatingActionButton: DynamicFAB( + isKeypadOpen: isKeypadOpen, + isFormValid: _passwordsMatch && _isPasswordValid, + buttonText: title, + onPressedFunction: () { + _updatePassword(); + FocusScope.of(context).unfocus(); + }, + ), + floatingActionButtonLocation: fabLocation(), + floatingActionButtonAnimator: NoScalingAnimation(), + ); + } + + Widget _getBody(String buttonTextAndHeading) { + final email = widget.sessions.user.email; + var passwordStrengthText = context.strings.weakStrength; + var passwordStrengthColor = Colors.redAccent; + if (_passwordStrength > kStrongPasswordStrengthThreshold) { + passwordStrengthText = context.strings.strongStrength; + passwordStrengthColor = Colors.greenAccent; + } else if (_passwordStrength > kMildPasswordStrengthThreshold) { + passwordStrengthText = context.strings.moderateStrength; + passwordStrengthColor = Colors.orangeAccent; + } + return Column( + children: [ + Expanded( + child: AutofillGroup( + child: ListView( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + child: Text( + buttonTextAndHeading, + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + "Enter new password for $email account. You will be able " + "to use this password to login into $email account.", + textAlign: TextAlign.start, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 14), + ), + ), + const Padding(padding: EdgeInsets.all(12)), + Visibility( + // hidden textForm for suggesting auto-fill service for saving + // password + visible: false, + child: TextFormField( + autofillHints: const [ + AutofillHints.email, + ], + autocorrect: false, + keyboardType: TextInputType.emailAddress, + initialValue: email, + textInputAction: TextInputAction.next, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: TextFormField( + autofillHints: const [AutofillHints.newPassword], + decoration: InputDecoration( + fillColor: + _isPasswordValid ? _validFieldValueColor : null, + filled: true, + hintText: context.strings.password, + contentPadding: const EdgeInsets.all(20), + border: UnderlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(6), + ), + suffixIcon: _password1InFocus + ? IconButton( + icon: Icon( + _password1Visible + ? Icons.visibility + : Icons.visibility_off, + color: Theme.of(context).iconTheme.color, + size: 20, + ), + onPressed: () { + setState(() { + _password1Visible = !_password1Visible; + }); + }, + ) + : null, + ), + obscureText: !_password1Visible, + controller: _passwordController1, + autofocus: false, + autocorrect: false, + keyboardType: TextInputType.visiblePassword, + onChanged: (password) { + setState(() { + _passwordInInputBox = password; + _passwordStrength = estimatePasswordStrength(password); + _isPasswordValid = + _passwordStrength >= kMildPasswordStrengthThreshold; + _passwordsMatch = _passwordInInputBox == + _passwordInInputConfirmationBox; + }); + }, + textInputAction: TextInputAction.next, + focusNode: _password1FocusNode, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: TextFormField( + keyboardType: TextInputType.visiblePassword, + controller: _passwordController2, + obscureText: !_password2Visible, + autofillHints: const [AutofillHints.newPassword], + onEditingComplete: () => TextInput.finishAutofillContext(), + decoration: InputDecoration( + fillColor: _passwordsMatch ? _validFieldValueColor : null, + filled: true, + hintText: context.strings.confirmPassword, + contentPadding: const EdgeInsets.symmetric( + vertical: 20, + horizontal: 20, + ), + suffixIcon: _password2InFocus + ? IconButton( + icon: Icon( + _password2Visible + ? Icons.visibility + : Icons.visibility_off, + color: Theme.of(context).iconTheme.color, + size: 20, + ), + onPressed: () { + setState(() { + _password2Visible = !_password2Visible; + }); + }, + ) + : null, + border: UnderlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(6), + ), + ), + focusNode: _password2FocusNode, + onChanged: (cnfPassword) { + setState(() { + _passwordInInputConfirmationBox = cnfPassword; + if (_passwordInInputBox != '') { + _passwordsMatch = _passwordInInputBox == + _passwordInInputConfirmationBox; + } + }); + }, + ), + ), + Opacity( + opacity: + (_passwordInInputBox != '') && _password1InFocus ? 1 : 0, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Text( + "Password strength: $passwordStrengthText", + style: TextStyle( + color: passwordStrengthColor, + ), + ), + ), + ), + const SizedBox(height: 8), + const Padding(padding: EdgeInsets.all(20)), + ], + ), + ), + ), + ], + ); + } + + void _updatePassword() async { + final dialog = createProgressDialog( + context, + context.strings.generatingEncryptionKeys, + ); + await dialog.show(); + try { + final String password = _passwordController1.text; + final KeyAttributes attributes = widget.attributes; + Uint8List? masterKey; + try { + // Decrypt the master key that was earlier encrypted with the recovery key + masterKey = await CryptoUtil.decrypt( + CryptoUtil.base642bin(attributes.masterKeyEncryptedWithRecoveryKey), + CryptoUtil.hex2bin(widget.recoveryKey), + CryptoUtil.base642bin(attributes.masterKeyDecryptionNonce), + ); + } catch (e) { + _logger.severe(e, "Failed to get master key using recoveryKey"); + rethrow; + } + + // Derive a key from the password that will be used to encrypt and + // decrypt the master key + final kekSalt = CryptoUtil.getSaltToDeriveKey(); + final derivedKeyResult = await CryptoUtil.deriveSensitiveKey( + utf8.encode(password), + kekSalt, + ); + final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key); + // Encrypt the key with this derived key + final encryptedKeyData = + CryptoUtil.encryptSync(masterKey, derivedKeyResult.key); + + final updatedAttributes = attributes.copyWith( + kekSalt: CryptoUtil.bin2base64(kekSalt), + encryptedKey: CryptoUtil.bin2base64(encryptedKeyData.encryptedData!), + keyDecryptionNonce: CryptoUtil.bin2base64(encryptedKeyData.nonce!), + memLimit: derivedKeyResult.memLimit, + opsLimit: derivedKeyResult.opsLimit, + ); + final setKeyRequest = SetKeysRequest( + kekSalt: updatedAttributes.kekSalt, + encryptedKey: updatedAttributes.encryptedKey, + keyDecryptionNonce: updatedAttributes.keyDecryptionNonce, + memLimit: updatedAttributes.memLimit, + opsLimit: updatedAttributes.opsLimit, + ); + await EmergencyContactService.instance.changePasswordForOther( + Uint8List.fromList(loginKey), + setKeyRequest, + widget.sessions, + ); + await dialog.hide(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.strings.passwordChangedSuccessfully), + backgroundColor: Colors.green, + ), + ); + Navigator.of(context).pop(); + } catch (e, s) { + _logger.severe(e, s); + await dialog.hide(); + await showGenericErrorDialog(context: context, error: e); + } + } + + @override + void dispose() { + _passwordController1.dispose(); + _passwordController2.dispose(); + _password1FocusNode.dispose(); + _password2FocusNode.dispose(); + super.dispose(); + } +} diff --git a/mobile/packages/legacy/lib/pages/select_contact_page.dart b/mobile/packages/legacy/lib/pages/select_contact_page.dart new file mode 100644 index 0000000000..58fc9cefd2 --- /dev/null +++ b/mobile/packages/legacy/lib/pages/select_contact_page.dart @@ -0,0 +1,334 @@ +import "package:email_validator/email_validator.dart"; +import "package:ente_configuration/base_configuration.dart"; +import "package:ente_legacy/models/emergency_models.dart"; +import "package:ente_legacy/services/emergency_service.dart"; +import "package:ente_sharing/models/user.dart"; +import "package:ente_sharing/verify_identity_dialog.dart"; +import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/components/buttons/button_widget.dart"; +import "package:ente_ui/components/buttons/models/button_type.dart"; +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/divider_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/components/menu_section_description_widget.dart"; +import "package:ente_ui/components/menu_section_title.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_ui/utils/dialog_util.dart"; +import "package:flutter/material.dart"; +import "package:logging/logging.dart"; + +class AddContactPage extends StatefulWidget { + final EmergencyInfo emergencyInfo; + final BaseConfiguration config; + + const AddContactPage( + this.emergencyInfo, { + super.key, + required this.config, + }); + + @override + State createState() => _AddContactPageState(); +} + +class _AddContactPageState extends State { + String selectedEmail = ''; + String _email = ''; + bool isEmailListEmpty = false; + bool _emailIsValid = false; + bool isKeypadOpen = false; + late final Logger _logger = Logger('AddContactPage'); + + // Focus nodes are necessary + final textFieldFocusNode = FocusNode(); + final _textController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + final List suggestedUsers = _getSuggestedUser(); + isEmailListEmpty = suggestedUsers.isEmpty; + + return Scaffold( + resizeToAvoidBottomInset: isKeypadOpen, + appBar: AppBar( + title: Text( + context.strings.addTrustedContact, + ), + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + context.strings.addANewEmail, + style: textTheme.small.copyWith(color: colorScheme.textMuted), + ), + ), + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _getEmailField(), + ), + if (isEmailListEmpty) + const Expanded(child: SizedBox.shrink()) + else + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + !isEmailListEmpty + ? MenuSectionTitle( + title: context.strings.orPickAnExistingOne, + ) + : const SizedBox.shrink(), + Expanded( + child: ListView.builder( + itemBuilder: (context, index) { + if (index >= suggestedUsers.length) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + ), + child: MenuSectionDescriptionWidget( + content: context.strings.whyAddTrustContact, + ), + ); + } + final currentUser = suggestedUsers[index]; + return Column( + children: [ + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: currentUser.email, + ), + leadingIconSize: 24.0, + leadingIconWidget: CircleAvatar( + radius: 12, + child: Text( + currentUser.email + .substring(0, 1) + .toUpperCase(), + style: const TextStyle(fontSize: 12), + ), + ), + menuItemColor: colorScheme.fillFaint, + pressedColor: colorScheme.fillFaint, + trailingIcon: + (selectedEmail == currentUser.email) + ? Icons.check + : null, + onTap: () async { + textFieldFocusNode.unfocus(); + if (selectedEmail == currentUser.email) { + selectedEmail = ''; + } else { + selectedEmail = currentUser.email; + } + setState(() {}); + }, + isTopBorderRadiusRemoved: index > 0, + isBottomBorderRadiusRemoved: + index < (suggestedUsers.length - 1), + ), + (index == (suggestedUsers.length - 1)) + ? const SizedBox.shrink() + : DividerWidget( + dividerType: DividerType.menu, + bgColor: colorScheme.fillFaint, + ), + ], + ); + }, + itemCount: suggestedUsers.length + 1, + ), + ), + ], + ), + ), + ), + SafeArea( + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 16, + right: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + ButtonWidget( + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + labelText: "Add", + isDisabled: (selectedEmail == '' && !_emailIsValid), + onTap: (selectedEmail == '' && !_emailIsValid) + ? null + : () async { + final emailToAdd = + selectedEmail == '' ? _email : selectedEmail; + final choiceResult = await showChoiceActionSheet( + context, + title: context.strings.warning, + body: context.strings.confirmAddingTrustedContact( + emailToAdd, + 30, + ), + firstButtonLabel: context.strings.proceed, + isCritical: true, + ); + if (choiceResult != null && + choiceResult.action == ButtonAction.first) { + try { + final r = await EmergencyContactService.instance + .addContact(context, emailToAdd); + if (r && mounted) { + Navigator.of(context).pop(true); + } + } catch (e) { + _logger.severe('Failed to add contact', e); + await showErrorDialog( + context, + context.strings.error, + context.strings.somethingWentWrong, + ); + } + } + }, + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () async { + if ((selectedEmail == '' && !_emailIsValid)) { + await showErrorDialog( + context, + context.strings.invalidEmailAddress, + context.strings.enterValidEmail, + ); + return; + } + final emailToAdd = + selectedEmail == '' ? _email : selectedEmail; + await showDialog( + context: context, + builder: (BuildContext context) { + return VerifyIdentityDialog( + self: false, + email: emailToAdd, + config: widget.config, + ); + }, + ); + }, + child: Text( + context.strings.verifyIDLabel, + textAlign: TextAlign.center, + style: textTheme.smallMuted.copyWith( + decoration: TextDecoration.underline, + ), + ), + ), + const SizedBox(height: 12), + ], + ), + ), + ), + ], + ), + ); + } + + void clearFocus() { + _textController.clear(); + _email = _textController.text; + _emailIsValid = false; + textFieldFocusNode.unfocus(); + setState(() {}); + } + + Widget _getEmailField() { + final colorScheme = getEnteColorScheme(context); + return TextFormField( + controller: _textController, + focusNode: textFieldFocusNode, + autofillHints: const [AutofillHints.email], + decoration: InputDecoration( + focusedBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + borderSide: BorderSide(color: colorScheme.strokeMuted), + ), + fillColor: colorScheme.fillFaint, + filled: true, + hintText: context.strings.enterEmail, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: UnderlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(4), + ), + prefixIcon: Icon( + Icons.email_outlined, + color: colorScheme.strokeMuted, + ), + suffixIcon: _email == '' + ? null + : IconButton( + onPressed: clearFocus, + icon: Icon( + Icons.cancel, + color: colorScheme.strokeMuted, + ), + ), + ), + onChanged: (value) { + if (selectedEmail != '') { + selectedEmail = ''; + } + _email = value.trim(); + _emailIsValid = EmailValidator.validate(_email); + setState(() {}); + }, + autocorrect: false, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + ); + } + + List _getSuggestedUser() { + final List suggestedUsers = []; + // For now, return an empty list since we don't have access to CollectionsService + // In a real implementation, this would fetch users from shared collections + + if (_textController.text.trim().isNotEmpty) { + suggestedUsers.removeWhere( + (element) => !element.email + .toLowerCase() + .contains(_textController.text.trim().toLowerCase()), + ); + } + suggestedUsers.sort((a, b) => a.email.compareTo(b.email)); + + return suggestedUsers; + } +} diff --git a/mobile/packages/legacy/lib/services/emergency_service.dart b/mobile/packages/legacy/lib/services/emergency_service.dart new file mode 100644 index 0000000000..6bc48c2b3c --- /dev/null +++ b/mobile/packages/legacy/lib/services/emergency_service.dart @@ -0,0 +1,298 @@ +import "dart:convert"; +import "dart:math"; +import "dart:typed_data"; + +import "package:dio/dio.dart"; +import "package:ente_accounts/models/set_keys_request.dart"; +import "package:ente_accounts/models/srp.dart"; +import "package:ente_accounts/services/user_service.dart"; +import "package:ente_base/models/key_attributes.dart"; +import "package:ente_configuration/base_configuration.dart"; +import "package:ente_crypto_dart/ente_crypto_dart.dart"; +import "package:ente_legacy/models/emergency_models.dart"; +import "package:ente_network/network.dart"; +import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/components/user_dialogs.dart"; +import "package:ente_utils/email_util.dart"; +import "package:flutter/material.dart"; +import "package:logging/logging.dart"; +import "package:pointycastle/pointycastle.dart"; +import "package:pointycastle/random/fortuna_random.dart"; +import "package:pointycastle/srp/srp6_client.dart"; +import "package:pointycastle/srp/srp6_standard_groups.dart"; +import "package:pointycastle/srp/srp6_util.dart"; +import "package:pointycastle/srp/srp6_verifier_generator.dart"; +import "package:uuid/uuid.dart"; + +class EmergencyContactService { + final Dio _enteDio = Network.instance.enteDio; + late UserService _userService; + late BaseConfiguration _config; + late final Logger _logger = Logger("EmergencyContactService"); + + EmergencyContactService._privateConstructor(); + static final EmergencyContactService instance = + EmergencyContactService._privateConstructor(); + + Future init( + UserService userService, + BaseConfiguration config, + ) async { + _userService = userService; + _config = config; + } + + Future addContact(BuildContext context, String email) async { + if (!isValidEmail(email)) { + await showErrorDialog( + context, + context.strings.invalidEmailAddress, + context.strings.enterValidEmail, + ); + return false; + } else if (email.trim() == _config.getEmail()) { + await showErrorDialog( + context, + context.strings.oops, + context.strings.youCannotShareWithYourself, + ); + return false; + } + final String? publicKey = await _userService.getPublicKey(email); + if (publicKey == null) { + await showInviteDialog(context, email); + return false; + } + final Uint8List recoveryKey = _config.getRecoveryKey(); + final encryptedKey = CryptoUtil.sealSync( + recoveryKey, + CryptoUtil.base642bin(publicKey), + ); + await _enteDio.post( + "/emergency-contacts/add", + data: { + "email": email.trim(), + "encryptedKey": CryptoUtil.bin2base64(encryptedKey), + }, + ); + return true; + } + + Future getInfo() async { + try { + final response = await _enteDio.get("/emergency-contacts/info"); + return EmergencyInfo.fromJson(response.data); + } catch (e, s) { + Logger("EmergencyContact").severe('failed to get info', e, s); + rethrow; + } + } + + Future updateContact( + EmergencyContact contact, + ContactState state, + ) async { + try { + await _enteDio.post( + "/emergency-contacts/update", + data: { + "userID": contact.user.id, + "emergencyContactID": contact.emergencyContact.id, + "state": state.stringValue, + }, + ); + } catch (e, s) { + Logger("EmergencyContact").severe('failed to update contact', e, s); + rethrow; + } + } + + Future startRecovery(EmergencyContact contact) async { + try { + await _enteDio.post( + "/emergency-contacts/start-recovery", + data: { + "userID": contact.user.id, + "emergencyContactID": contact.emergencyContact.id, + }, + ); + } catch (e, s) { + Logger("EmergencyContact").severe('failed to start recovery', e, s); + rethrow; + } + } + + Future stopRecovery(RecoverySessions session) async { + try { + await _enteDio.post( + "/emergency-contacts/stop-recovery", + data: { + "userID": session.user.id, + "emergencyContactID": session.emergencyContact.id, + "id": session.id, + }, + ); + } catch (e, s) { + Logger("EmergencyContact").severe('failed to stop recovery', e, s); + rethrow; + } + } + + Future rejectRecovery(RecoverySessions session) async { + try { + await _enteDio.post( + "/emergency-contacts/reject-recovery", + data: { + "userID": session.user.id, + "emergencyContactID": session.emergencyContact.id, + "id": session.id, + }, + ); + } catch (e, s) { + Logger("EmergencyContact").severe('failed to stop recovery', e, s); + rethrow; + } + } + + Future approveRecovery(RecoverySessions session) async { + try { + await _enteDio.post( + "/emergency-contacts/approve-recovery", + data: { + "userID": session.user.id, + "emergencyContactID": session.emergencyContact.id, + "id": session.id, + }, + ); + } catch (e, s) { + Logger("EmergencyContact").severe('failed to approve recovery', e, s); + rethrow; + } + } + + Future<(String, KeyAttributes)> getRecoveryInfo( + RecoverySessions sessions, + ) async { + try { + final resp = await _enteDio.get( + "/emergency-contacts/recovery-info/${sessions.id}", + ); + final String encryptedKey = resp.data["encryptedKey"]!; + final decryptedKey = CryptoUtil.openSealSync( + CryptoUtil.base642bin(encryptedKey), + CryptoUtil.base642bin(_config.getKeyAttributes()!.publicKey), + _config.getSecretKey()!, + ); + final String hexRecoveryKey = CryptoUtil.bin2hex(decryptedKey); + final KeyAttributes keyAttributes = + KeyAttributes.fromMap(resp.data['userKeyAttr']); + return (hexRecoveryKey, keyAttributes); + } catch (e, s) { + Logger("EmergencyContact").severe('failed to stop recovery', e, s); + rethrow; + } + } + + Future changePasswordForOther( + Uint8List loginKey, + SetKeysRequest setKeysRequest, + RecoverySessions recoverySessions, + ) async { + try { + final SRP6GroupParameters kDefaultSrpGroup = + SRP6StandardGroups.rfc5054_4096; + final String username = const Uuid().v4().toString(); + final SecureRandom random = _getSecureRandom(); + final Uint8List identity = Uint8List.fromList(utf8.encode(username)); + final Uint8List password = loginKey; + final Uint8List salt = random.nextBytes(16); + final gen = SRP6VerifierGenerator( + group: kDefaultSrpGroup, + digest: Digest('SHA-256'), + ); + final v = gen.generateVerifier(salt, identity, password); + + final client = SRP6Client( + group: kDefaultSrpGroup, + digest: Digest('SHA-256'), + random: random, + ); + + final A = client.generateClientCredentials(salt, identity, password); + final request = SetupSRPRequest( + srpUserID: username, + srpSalt: base64Encode(salt), + srpVerifier: base64Encode(SRP6Util.encodeBigInt(v)), + srpA: base64Encode(SRP6Util.encodeBigInt(A!)), + isUpdate: false, + ); + final response = await _enteDio.post( + "/emergency-contacts/init-change-password", + data: { + "recoveryID": recoverySessions.id, + "setupSRPRequest": request.toMap(), + }, + ); + if (response.statusCode == 200) { + final SetupSRPResponse setupSRPResponse = + SetupSRPResponse.fromJson(response.data); + final serverB = + SRP6Util.decodeBigInt(base64Decode(setupSRPResponse.srpB)); + + // ignore: unused_local_variable + final clientS = client.calculateSecret(serverB); + final clientM = client.calculateClientEvidenceMessage(); + // ignore: unused_local_variable + late Response srpCompleteResponse; + srpCompleteResponse = await _enteDio.post( + "/emergency-contacts/change-password", + data: { + "recoveryID": recoverySessions.id, + 'updateSrpAndKeysRequest': { + 'setupID': setupSRPResponse.setupID, + 'srpM1': base64Encode(SRP6Util.encodeBigInt(clientM!)), + 'updatedKeyAttr': setKeysRequest.toMap(), + }, + }, + ); + } else { + throw Exception("register-srp action failed"); + } + } catch (e, s) { + _logger.severe("failed to change password for other", e, s); + rethrow; + } + } + + SecureRandom _getSecureRandom() { + final List seeds = []; + final random = Random.secure(); + for (int i = 0; i < 32; i++) { + seeds.add(random.nextInt(255)); + } + final secureRandom = FortunaRandom(); + secureRandom.seed(KeyParameter(Uint8List.fromList(seeds))); + return secureRandom; + } + + // Helper methods for dialogs + Future showErrorDialog( + BuildContext context, + String title, + String message, + ) async { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } +} diff --git a/mobile/packages/legacy/pubspec.lock b/mobile/packages/legacy/pubspec.lock new file mode 100644 index 0000000000..6c7ffdbb7b --- /dev/null +++ b/mobile/packages/legacy/pubspec.lock @@ -0,0 +1,1366 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + app_links: + dependency: transitive + description: + name: app_links + sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + bip39: + dependency: transitive + description: + name: bip39 + sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc + url: "https://pub.dev" + source: hosted + version: "1.0.6" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: "direct main" + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cronet_http: + dependency: transitive + description: + name: cronet_http + sha256: "1b99ad5ae81aa9d2f12900e5f17d3681f3828629bb7f7fe7ad88076a34209840" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_http: + dependency: transitive + description: + name: cupertino_http + sha256: "72187f715837290a63479a5b0ae709f4fedad0ed6bd0441c275eceaa02d5abae" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + dotted_border: + dependency: transitive + description: + name: dotted_border + sha256: "99b091ec6891ba0c5331fdc2b502993c7c108f898995739a73c6845d71dad70c" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + email_validator: + dependency: "direct main" + description: + name: email_validator + sha256: b19aa5d92fdd76fbc65112060c94d45ba855105a28bb6e462de7ff03b12fa1fb + url: "https://pub.dev" + source: hosted + version: "3.0.0" + ente_accounts: + dependency: "direct main" + description: + path: "../accounts" + relative: true + source: path + version: "1.0.0" + ente_base: + dependency: "direct main" + description: + path: "../base" + relative: true + source: path + version: "1.0.0" + ente_configuration: + dependency: "direct main" + description: + path: "../configuration" + relative: true + source: path + version: "1.0.0" + ente_crypto_dart: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: f91e1545f8263df127762240c4da54a0c42835b2 + url: "https://github.com/ente-io/ente_crypto_dart.git" + source: git + version: "1.0.0" + ente_events: + dependency: "direct overridden" + description: + path: "../events" + relative: true + source: path + version: "1.0.0" + ente_lock_screen: + dependency: "direct overridden" + description: + path: "../lock_screen" + relative: true + source: path + version: "1.0.0" + ente_logging: + dependency: "direct overridden" + description: + path: "../logging" + relative: true + source: path + version: "1.0.0" + ente_network: + dependency: "direct main" + description: + path: "../network" + relative: true + source: path + version: "1.0.0" + ente_sharing: + dependency: "direct main" + description: + path: "../sharing" + relative: true + source: path + version: "1.0.0" + ente_strings: + dependency: "direct main" + description: + path: "../strings" + relative: true + source: path + version: "1.0.0" + ente_ui: + dependency: "direct main" + description: + path: "../ui" + relative: true + source: path + version: "1.0.0" + ente_utils: + dependency: "direct main" + description: + path: "../utils" + relative: true + source: path + version: "1.0.0" + event_bus: + dependency: transitive + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + expandable: + dependency: transitive + description: + name: expandable + sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + fast_base58: + dependency: transitive + description: + name: fast_base58 + sha256: "611f65633b734f27a850b51371b3eba993a5165650e12e8e7b02959f3768ba06" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_saver: + dependency: transitive + description: + name: file_saver + sha256: "9d93db09bd4da9e43238f9dd485360fc51a5c138eea5ef5f407ec56e58079ac0" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_animate: + dependency: transitive + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" + flutter_email_sender: + dependency: transitive + description: + name: flutter_email_sender + sha256: d39eb5e91358fc19ec4050da69accec21f9d5b2b6bcf188aa246327b6ca2352c + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter_inappwebview: + dependency: transitive + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_local_authentication: + dependency: transitive + description: + path: "." + ref: "1ac346a04592a05fd75acccf2e01fa3c7e955d96" + resolved-ref: "1ac346a04592a05fd75acccf2e01fa3c7e955d96" + url: "https://github.com/eaceto/flutter_local_authentication" + source: git + version: "1.2.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31 + url: "https://pub.dev" + source: hosted + version: "2.0.30" + flutter_secure_storage: + dependency: transitive + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: transitive + description: + name: fluttertoast + sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" + url: "https://pub.dev" + source: hosted + version: "8.2.12" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + hex: + dependency: transitive + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: transitive + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + http_profile: + dependency: transitive + description: + name: http_profile + sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + jni: + dependency: transitive + description: + name: jni + sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 + url: "https://pub.dev" + source: hosted + version: "0.14.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + local_auth: + dependency: transitive + description: + name: local_auth + sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3" + url: "https://pub.dev" + source: hosted + version: "1.0.52" + local_auth_darwin: + dependency: transitive + description: + name: local_auth_darwin + sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + modal_bottom_sheet: + dependency: transitive + description: + name: modal_bottom_sheet + sha256: eac66ef8cb0461bf069a38c5eb0fa728cee525a531a8304bd3f7b2185407c67e + url: "https://pub.dev" + source: hosted + version: "3.0.0" + native_dio_adapter: + dependency: transitive + description: + name: native_dio_adapter + sha256: "1c51bd42027861d27ccad462ba0903f5e3197461cc6d59a0bb8658cb5ad7bd01" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "9f034ba1eeca53ddb339bc8f4813cb07336a849cd735559b60cdc068ecce2dc7" + url: "https://pub.dev" + source: hosted + version: "7.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + password_strength: + dependency: "direct main" + description: + name: password_strength + sha256: "0e51e3d864e37873a1347e658147f88b66e141ee36c58e19828dc5637961e1ce" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" + url: "https://pub.dev" + source: hosted + version: "2.2.18" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + pinput: + dependency: transitive + description: + name: pinput + sha256: c41f42ee301505ae2375ec32871c985d3717bf8aee845620465b286e0140aad2 + url: "https://pub.dev" + source: hosted + version: "5.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: "direct main" + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + privacy_screen: + dependency: transitive + description: + name: privacy_screen + sha256: "2856e3a3ed082061a5cd2a1518f1ce6367c55916fb75e5db72e5983033a1ca54" + url: "https://pub.dev" + source: hosted + version: "0.0.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + sentry: + dependency: transitive + description: + name: sentry + sha256: "599701ca0693a74da361bc780b0752e1abc98226cf5095f6b069648116c896bb" + url: "https://pub.dev" + source: hosted + version: "8.14.2" + sentry_flutter: + dependency: transitive + description: + name: sentry_flutter + sha256: "5ba2cf40646a77d113b37a07bd69f61bb3ec8a73cbabe5537b05a7c89d2656f8" + url: "https://pub.dev" + source: hosted + version: "8.14.2" + share_plus: + dependency: transitive + description: + name: share_plus + sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1 + url: "https://pub.dev" + source: hosted + version: "11.1.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + sodium: + dependency: transitive + description: + name: sodium + sha256: d9830a388e37c82891888e64cfd4c6764fa3ac716bed80ac6eab89ee42c3cd76 + url: "https://pub.dev" + source: hosted + version: "2.3.1+1" + sodium_libs: + dependency: transitive + description: + name: sodium_libs + sha256: aa764acd6ccc6113e119c2d99471aeeb4637a9a501639549b297d3a143ff49b3 + url: "https://pub.dev" + source: hosted + version: "2.2.1+6" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + step_progress_indicator: + dependency: transitive + description: + name: step_progress_indicator + sha256: b51bb1fcfc78454359f0658c5a2c21548c3825ebf76e826308e9ca10f383bbb8 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + styled_text: + dependency: transitive + description: + name: styled_text + sha256: fd624172cf629751b4f171dd0ecf9acf02a06df3f8a81bb56c0caa4f1df706c3 + url: "https://pub.dev" + source: hosted + version: "8.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + ua_client_hints: + dependency: transitive + description: + name: ua_client_hints + sha256: "1b8759a46bfeab355252881df27f2604c01bded86aa2b578869fb1b638b23118" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" + url: "https://pub.dev" + source: hosted + version: "6.3.18" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + url: "https://pub.dev" + source: hosted + version: "1.1.5" + window_manager: + dependency: transitive + description: + name: window_manager + sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + xmlstream: + dependency: transitive + description: + name: xmlstream + sha256: cfc14e3f256997897df9481ae630d94c2d85ada5187ebeb868bb1aabc2c977b4 + url: "https://pub.dev" + source: hosted + version: "1.1.1" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/mobile/packages/legacy/pubspec.yaml b/mobile/packages/legacy/pubspec.yaml new file mode 100644 index 0000000000..a13f1bf49a --- /dev/null +++ b/mobile/packages/legacy/pubspec.yaml @@ -0,0 +1,49 @@ +name: ente_legacy +description: A Flutter package containing legacy services for Ente apps +version: 1.0.0 + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=1.17.0" + +dependencies: + collection: ^1.18.0 + dio: ^5.4.0 + email_validator: ^3.0.0 + ente_accounts: + path: ../accounts + ente_base: + path: ../base + ente_configuration: + path: ../configuration + ente_crypto_dart: + git: + url: https://github.com/ente-io/ente_crypto_dart.git + ente_network: + path: ../network + ente_sharing: + path: ../sharing + ente_strings: + path: ../strings + ente_ui: + path: ../ui + ente_utils: + path: ../utils + flutter: + sdk: flutter + flutter_svg: ^2.0.10+1 + intl: ^0.20.2 + logging: ^1.2.0 + password_strength: ^0.2.0 + pointycastle: ^3.7.3 + uuid: ^4.2.1 + +dev_dependencies: + flutter_lints: ^5.0.0 + flutter_test: + sdk: flutter + +flutter: + +# This package is not meant to be published +publish_to: none diff --git a/mobile/packages/legacy/pubspec_overrides.yaml b/mobile/packages/legacy/pubspec_overrides.yaml new file mode 100644 index 0000000000..6991995c69 --- /dev/null +++ b/mobile/packages/legacy/pubspec_overrides.yaml @@ -0,0 +1,24 @@ +# melos_managed_dependency_overrides: ente_accounts,ente_base,ente_configuration,ente_events,ente_lock_screen,ente_logging,ente_network,ente_sharing,ente_strings,ente_ui,ente_utils +dependency_overrides: + ente_accounts: + path: ../accounts + ente_base: + path: ../base + ente_configuration: + path: ../configuration + ente_events: + path: ../events + ente_lock_screen: + path: ../lock_screen + ente_logging: + path: ../logging + ente_network: + path: ../network + ente_sharing: + path: ../sharing + ente_strings: + path: ../strings + ente_ui: + path: ../ui + ente_utils: + path: ../utils diff --git a/mobile/packages/strings/lib/l10n/strings_localizations.dart b/mobile/packages/strings/lib/l10n/strings_localizations.dart index 4e7c857b7b..3d2314096f 100644 --- a/mobile/packages/strings/lib/l10n/strings_localizations.dart +++ b/mobile/packages/strings/lib/l10n/strings_localizations.dart @@ -1547,6 +1547,12 @@ abstract class StringsLocalizations { /// **'You are about to add {email} as a trusted contact. They will be able to recover your account if you are absent for {numOfDays} days.'** String confirmAddingTrustedContact(String email, int numOfDays); + /// No description provided for @youCannotShareWithYourself. + /// + /// In en, this message translates to: + /// **'You cannot share with yourself'** + String get youCannotShareWithYourself; + /// No description provided for @emailNoEnteAccount. /// /// In en, this message translates to: @@ -1564,6 +1570,12 @@ abstract class StringsLocalizations { /// In en, this message translates to: /// **'Hey, can you confirm that this is your ente.io verification ID: {verificationID}'** String shareTextConfirmOthersVerificationID(Object verificationID); + + /// No description provided for @inviteToEnte. + /// + /// In en, this message translates to: + /// **'Invite to Ente'** + String get inviteToEnte; } class _StringsLocalizationsDelegate