import "dart:async"; import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; import "package:flutter_svg/flutter_svg.dart"; import 'package:photos/core/configuration.dart'; import "package:photos/emergency/emergency_service.dart"; import "package:photos/emergency/model.dart"; import "package:photos/emergency/other_contact_page.dart"; import "package:photos/emergency/select_contact_page.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/theme/colors.dart"; import 'package:photos/theme/ente_theme.dart'; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/components/action_sheet_widget.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/components/notification_widget.dart"; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/components/title_bar_widget.dart'; import "package:photos/ui/sharing/user_avator_widget.dart"; import "package:photos/utils/navigation_util.dart"; import "package:photos/utils/toast_util.dart"; class EmergencyPage extends StatefulWidget { const EmergencyPage({ super.key, }); @override State createState() => _EmergencyPageState(); } class _EmergencyPageState extends State { late int currentUserID; EmergencyInfo? info; bool hasTrustedContact = false; @override void initState() { super.initState(); currentUserID = Configuration.instance.getUserID()!; // set info to null after 5 second 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, S.of(context).somethingWentWrong, ); } } @override Widget build(BuildContext context) { final colorScheme = getEnteColorScheme(context); final currentUserID = Configuration.instance.getUserID()!; final List othersTrustedContacts = info?.othersEmergencyContact ?? []; final List trustedContacts = info?.contacts ?? []; return Scaffold( body: CustomScrollView( primary: false, slivers: [ TitleBarWidget( flexibleSpaceTitle: TitleBarTitleWidget( title: S.of(context).legacy, ), ), if (info == null) const SliverFillRemaining( hasScrollBody: false, child: Center( child: EnteLoadingWidget(), ), ), if (info != null) if (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.l10n.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, ), 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: S.of(context).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, ), 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.l10n.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.l10n.legacyPageDesc2, style: getEnteTextTheme(context).smallMuted, ), const SizedBox(height: 16), ButtonWidget( buttonType: ButtonType.primary, labelText: S.of(context).addTrustedContact, shouldSurfaceExecutionStates: false, onTap: () async { await routeToPage( context, AddContactPage(info!), forceCustomPageRoute: true, ); unawaited(_fetchData()); }, ), ], ); } return MenuItemWidget( captionedTextWidget: CaptionedTextWidget( title: trustedContacts.isNotEmpty ? S.of(context).addMore : S.of(context).addTrustedContact, makeTextBold: true, ), leadingIcon: Icons.add_outlined, surfaceExecutionStates: false, menuItemColor: getEnteColorScheme(context).fillFaint, onTap: () async { await routeToPage( context, AddContactPage(info!), 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.l10n.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, ), 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!, ); }, ), ); // await routeToPage( // context, // OtherContactPage( // contact: currentUser, // emergencyInfo: info!, // ), // ); 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: S.of(context).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: S.of(context).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: S.of(context).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: S.of(context).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: S.of(context).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: S.of(context).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: S.of(context).cancel, buttonType: ButtonType.tertiary, buttonSize: ButtonSize.large, buttonAction: ButtonAction.third, shouldStickToDarkTheme: true, isInAlert: true, ), ], body: S.of(context).legacyInvite(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.l10n.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: S.of(context).cancel, buttonType: ButtonType.tertiary, buttonSize: ButtonSize.large, buttonAction: ButtonAction.third, shouldStickToDarkTheme: true, isInAlert: true, ), ], body: context.l10n.recoveryWarningBody(emergencyContactEmail), actionSheetType: ActionSheetType.defaultActionSheet, ); return; } }