Legacy package

This commit is contained in:
AmanRajSinghMourya
2025-09-06 06:52:21 +05:30
parent 0e0ba2d5af
commit 2932ee7d4c
12 changed files with 3447 additions and 0 deletions

View File

@@ -0,0 +1 @@
include: ../../analysis_options.yaml

View File

@@ -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';

View File

@@ -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<String, dynamic> 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<EmergencyContact> contacts;
// List of recovery sessions that are created to recover current user account
final List<RecoverySessions> recoverSessions;
// List of emergency contacts that have added current user as their emergency contact
final List<EmergencyContact> othersEmergencyContact;
// List of recovery sessions that are created to recover grantor's account
final List<RecoverySessions> othersRecoverySession;
EmergencyInfo(
this.contacts,
this.recoverSessions,
this.othersEmergencyContact,
this.othersRecoverySession,
);
// from json
EmergencyInfo.fromJson(Map<String, dynamic> 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<String, dynamic> json)
: id = json['id'],
user = User.fromMap(json['user']),
emergencyContact = User.fromMap(json['emergencyContact']),
status = json['status'],
waitTill = json['waitTill'],
createdAt = json['createdAt'];
}

View File

@@ -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<EmergencyPage> createState() => _EmergencyPageState();
}
class _EmergencyPageState extends State<EmergencyPage> {
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<void> _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<EmergencyContact> othersTrustedContacts =
info?.othersEmergencyContact ?? [];
final List<EmergencyContact> 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<void> 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<void> 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<void> 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;
}
}

View File

@@ -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<OtherContactPage> createState() => _OtherContactPageState();
}
class _OtherContactPageState extends State<OtherContactPage> {
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<void> _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<void> 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;
}
}

View File

@@ -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<RecoverOthersAccount> createState() => _RecoverOthersAccountState();
}
class _RecoverOthersAccountState extends State<RecoverOthersAccount> {
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();
}
}

View File

@@ -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<StatefulWidget> createState() => _AddContactPageState();
}
class _AddContactPageState extends State<AddContactPage> {
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<User> 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<User> _getSuggestedUser() {
final List<User> 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;
}
}

View File

@@ -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<void> init(
UserService userService,
BaseConfiguration config,
) async {
_userService = userService;
_config = config;
}
Future<bool> 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<EmergencyInfo> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<int> 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<void> 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'),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View File

@@ -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