1 import { useState } from 'react';
2 import { useLocation } from 'react-router-dom';
4 import { c } from 'ttag';
6 import { useAddresses } from '@proton/account/addresses/hooks';
7 import { useUser } from '@proton/account/user/hooks';
8 import { Button, Card, CircleLoader, Href, InlineLinkButton } from '@proton/atoms';
9 import Badge from '@proton/components/components/badge/Badge';
10 import Icon from '@proton/components/components/icon/Icon';
11 import AppLink from '@proton/components/components/link/AppLink';
12 import Info from '@proton/components/components/link/Info';
13 import useModalState from '@proton/components/components/modalTwo/useModalState';
14 import Tooltip from '@proton/components/components/tooltip/Tooltip';
15 import { PromotionBanner } from '@proton/components/containers/banner/PromotionBanner';
16 import useApi from '@proton/components/hooks/useApi';
17 import useConfig from '@proton/components/hooks/useConfig';
18 import useNotifications from '@proton/components/hooks/useNotifications';
19 import useLoading from '@proton/hooks/useLoading';
20 import { postVerifySend } from '@proton/shared/lib/api/verify';
21 import { getAppHref } from '@proton/shared/lib/apps/helper';
22 import { stripLocalBasenameFromPathname } from '@proton/shared/lib/authentication/pathnameHelper';
23 import type { APP_NAMES } from '@proton/shared/lib/constants';
28 CALENDAR_SHORT_APP_NAME,
33 } from '@proton/shared/lib/constants';
34 import { getIsAddressEnabled } from '@proton/shared/lib/helpers/address';
35 import { wait } from '@proton/shared/lib/helpers/promise';
36 import { stripLeadingAndTrailingSlash } from '@proton/shared/lib/helpers/string';
37 import type { Address } from '@proton/shared/lib/interfaces';
38 import { AddressConfirmationState, SessionRecoveryState, UserType } from '@proton/shared/lib/interfaces';
39 import { getIsExternalAccount } from '@proton/shared/lib/keys';
40 import clsx from '@proton/utils/clsx';
42 import { getVerificationSentText } from '../../containers/recovery/email/VerifyRecoveryEmailModal';
43 import getBoldFormattedText from '../../helpers/getBoldFormattedText';
44 import useSearchParamsEffect from '../../hooks/useSearchParamsEffect';
45 import { useIsSessionRecoveryAvailable, useSessionRecoveryState } from '../../hooks/useSessionRecovery';
46 import EditDisplayNameModal from './EditDisplayNameModal';
47 import EditExternalAddressModal from './EditExternalAddressModal';
48 import SettingsLayout from './SettingsLayout';
49 import SettingsLayoutLeft from './SettingsLayoutLeft';
50 import SettingsLayoutRight from './SettingsLayoutRight';
51 import SettingsSection from './SettingsSection';
52 import mailCalendar from './mail-calendar.svg';
53 import PasswordResetAvailableCard from './sessionRecovery/statusCards/PasswordResetAvailableCard';
54 import SessionRecoveryInProgressCard from './sessionRecovery/statusCards/SessionRecoveryInProgressCard';
55 import unverified from './unverified.svg';
61 const UsernameSection = ({ app }: Props) => {
62 const { APP_NAME } = useConfig();
64 const [loading, withLoading] = useLoading();
65 const { createNotification } = useNotifications();
66 const [user] = useUser();
67 const location = useLocation();
68 const [addresses, loadingAddresses] = useAddresses();
69 const [tmpAddress, setTmpAddress] = useState<Address>();
70 const [modalProps, setModalOpen, renderModal] = useModalState();
71 const [editAddressModalProps, setEditAddressModalOpen, renderEditAddressModal] = useModalState();
73 const [isSessionRecoveryAvailable] = useIsSessionRecoveryAvailable();
74 const sessionRecoveryStatus = useSessionRecoveryState();
76 const primaryAddress = addresses?.find(getIsAddressEnabled);
78 const BRAND_NAME_TWO = BRAND_NAME;
80 const fromPath = `/${stripLeadingAndTrailingSlash(stripLocalBasenameFromPathname(location.pathname))}`;
82 const handleSendVerificationEmail = async (destination: string) => {
86 Type: 'external_email',
87 Destination: destination,
92 text: getVerificationSentText(destination),
96 const isPrimaryAddressExternal = primaryAddress?.Type === ADDRESS_TYPE.TYPE_EXTERNAL;
97 const isExternalUser = getIsExternalAccount(user);
98 const isExternalUserAndPrimaryAddressExternal = isExternalUser && isPrimaryAddressExternal;
100 const isPrimaryAddressExternalAndVerified =
101 isPrimaryAddressExternal &&
102 primaryAddress?.ConfirmationState === AddressConfirmationState.CONFIRMATION_CONFIRMED;
104 const canVerifyExternalAddress = isExternalUserAndPrimaryAddressExternal && !isPrimaryAddressExternalAndVerified;
106 const canEditExternalAddress = canVerifyExternalAddress && (user.isPrivate || user.isAdmin);
108 const canSetupProtonAddress = isExternalUser && user.isPrivate && APP_NAME === APPS.PROTONACCOUNT;
110 useSearchParamsEffect(
112 if (!canEditExternalAddress || !primaryAddress) {
115 const actionParam = params.get('action');
120 if (actionParam === 'edit-email') {
121 params.delete('action');
122 setTmpAddress(primaryAddress);
123 setEditAddressModalOpen(true);
132 {renderModal && tmpAddress && <EditDisplayNameModal {...modalProps} address={tmpAddress} />}
133 {renderEditAddressModal && tmpAddress && (
134 <EditExternalAddressModal {...editAddressModalProps} address={tmpAddress} />
137 {isSessionRecoveryAvailable && sessionRecoveryStatus === SessionRecoveryState.GRACE_PERIOD && (
138 <SessionRecoveryInProgressCard className="mb-6" />
140 {isSessionRecoveryAvailable && sessionRecoveryStatus === SessionRecoveryState.INSECURE && (
141 <PasswordResetAvailableCard className="mb-6" />
143 {canSetupProtonAddress && (
144 <div className="mb-6">
146 toApp={APPS.PROTONACCOUNT}
147 to={`${SETUP_ADDRESS_PATH}?to=${APPS.PROTONMAIL}&from=${app}&from-type=settings&from-path=${fromPath}`}
148 className="text-no-decoration"
149 data-testid="get-proton-address"
154 contentCentered={false}
155 icon={<img width="40" src={mailCalendar} alt="" className="shrink-0" />}
156 description={getBoldFormattedText(
158 .t`**Get a ${BRAND_NAME} address** to use all ${BRAND_NAME_TWO} services including ${MAIL_SHORT_APP_NAME} and ${CALENDAR_SHORT_APP_NAME}.`
161 <div className="mr-4">
162 <Icon name="chevron-right" size={4} />
170 {canVerifyExternalAddress && (
171 <Card className="mb-8" rounded bordered={true} background={false}>
172 <div className="h3 text-bold mb-6">{c('Info').t`Secure your ${BRAND_NAME} Account`}</div>
173 <div className="flex gap-4 flex-nowrap items-start">
174 <img className="shrink-0" width="40" height="40" src={unverified} alt="" />
176 <div className="mb-2 text-lg text-semibold flex">
177 <div className="mr-2 text-ellipsis">{primaryAddress.Email}</div>
178 <Badge type="warning">{c('Info').t`Unverified`}</Badge>
181 <div className="mb-2">
182 {c('Info').t`Increase your account security by verifying your email address.`}
188 withLoading(handleSendVerificationEmail(primaryAddress.Email));
191 {c('Info').t`Resend verification email`}
201 <div className="text-semibold">{c('Label').t`Username`}</div>
202 </SettingsLayoutLeft>
203 <SettingsLayoutRight className="pt-2">
204 {isExternalUserAndPrimaryAddressExternal ? (
206 {isPrimaryAddressExternalAndVerified ? (
207 <div className="flex">
208 {primaryAddress.Email}
209 <Tooltip title={c('Tooltip').t`Verified email address`} openDelay={0}>
211 name="checkmark-circle-filled"
213 className="ml-2 color-success self-center"
219 {canEditExternalAddress ? (
220 <div className="flex">
221 <span className="mr-2">{primaryAddress.Email}</span>
225 setTmpAddress(primaryAddress);
226 setEditAddressModalOpen(true);
228 aria-label={c('Action').t`Edit email address`}
230 {c('Action').t`Edit`}
233 className="self-center"
235 .t`You can edit this once to ensure the correct email address for verification.`}
239 <div>{primaryAddress.Email}</div>
241 <div className="flex">
243 name="exclamation-circle-filled"
245 className="mr-1 color-danger self-center"
247 <span className="color-weak mr-1">
248 {c('Info').t`Unverified email address.`}
257 </SettingsLayoutRight>
259 {(primaryAddress || loadingAddresses) && (
262 <div className="text-semibold">{c('Label').t`Display name`}</div>
263 </SettingsLayoutLeft>
264 <SettingsLayoutRight className="pt-2">
265 {!primaryAddress || loadingAddresses ? (
266 <div className="flex flex-nowrap">
270 <div className="flex flex-nowrap">
273 'text-ellipsis user-select',
274 primaryAddress.DisplayName && 'mr-2'
277 {primaryAddress.DisplayName}
281 setTmpAddress(primaryAddress);
284 aria-label={c('Action').t`Edit display name`}
286 {c('Action').t`Edit`}
290 </SettingsLayoutRight>
293 {APP_NAME === APPS.PROTONVPN_SETTINGS && user.Type === UserType.PROTON && (
296 <div className="text-semibold">{c('Label').t`${MAIL_APP_NAME} address`}</div>
297 </SettingsLayoutLeft>
298 <SettingsLayoutRight className="pt-2">
300 if (loadingAddresses) {
302 <div className="flex flex-nowrap">
308 if (primaryAddress?.Email) {
310 <div className="text-pre-wrap break user-select">{primaryAddress.Email}</div>
316 href={`${getAppHref(SSO_PATHS.SWITCH, APPS.PROTONACCOUNT)}?product=mail`}
317 title={c('Info').t`Sign in to ${MAIL_APP_NAME} to activate your address`}
319 {c('Link').t`Not activated`}
323 </SettingsLayoutRight>
331 export default UsernameSection;