frontend/src/pages/accounts/settings.page.tsx (344 lines of code) (raw):

import type { NextPage } from "next"; import { FormEventHandler, MouseEventHandler, useEffect, useReducer, useRef, useState, } from "react"; import { toast } from "react-toastify"; import styles from "./settings.module.scss"; import { Layout } from "../../components/layout/Layout"; import { Banner } from "../../components/Banner"; import { useProfiles } from "../../hooks/api/profile"; import { InfoTriangleIcon, HideIcon, NewTabIcon, PerformanceIcon, CopyIcon, SupportIcon, ContactIcon, } from "../../components/Icons"; import { Button } from "../../components/Button"; import { getRuntimeConfig } from "../../config"; import { useLocalLabels } from "../../hooks/localLabels"; import { AliasData, useAliases } from "../../hooks/api/aliases"; import { useRuntimeData } from "../../hooks/api/runtimeData"; import { useAddonData } from "../../hooks/addon"; import { isFlagActive } from "../../functions/waffle"; import { isPhonesAvailableInCountry } from "../../functions/getPlan"; import { useL10n } from "../../hooks/l10n"; const Settings: NextPage = () => { const runtimeData = useRuntimeData(); const profileData = useProfiles(); const l10n = useL10n(); const [localLabels] = useLocalLabels(); const aliasData = useAliases(); const addonData = useAddonData(); const [labelCollectionEnabled, setLabelCollectionEnabled] = useState( profileData.data?.[0].server_storage, ); const [trackerRemovalEnabled, setTrackerRemovalEnabled] = useState( profileData.data?.[0].remove_level_one_email_trackers, ); const [phoneCallerSMSLogEnabled, setPhoneCallerSMSLogEnabled] = useState( profileData.data?.[0].store_phone_log, ); const [phoneCallerSMSLogEnabledLabel, setPhoneCallerSMSLogEnabledLabel] = useState(false); const [justCopiedApiKey, setJustCopiedApiKey] = useState(false); const apiKeyElementRef = useRef<HTMLInputElement>(null); const [ labelCollectionDisabledWarningToggles, countLabelCollectionDisabledWarningToggle, ] = useReducer((c) => c + 1, 0); useEffect(() => { countLabelCollectionDisabledWarningToggle(); }, [labelCollectionEnabled]); if (!profileData.isValidating && profileData.error) { document.location.assign(getRuntimeConfig().fxaLoginUrl); } if (!profileData.data || !runtimeData.data) { // TODO: Show a loading spinner? return null; } const profile = profileData.data[0]; const currentSettingWarning = profile.server_storage ? null : ( <div className={styles["banner-wrapper"]}> <Banner title={l10n.getString("settings-warning-collection-off-heading-3")} type="warning" > {l10n.getString("settings-warning-collection-off-description-3")} </Banner> </div> ); // This warning should only be shown when data collection is explicitly toggled off, // i.e. not when it is off on page load. const labelCollectionWarning = labelCollectionDisabledWarningToggles > 1 && !labelCollectionEnabled ? ( <div role="alert" className={styles["field-warning"]}> <InfoTriangleIcon alt="" /> <p>{l10n.getString("setting-label-collection-off-warning-3")}</p> </div> ) : null; const saveSettings: FormEventHandler = async (event) => { event.preventDefault(); try { await profileData.update(profile.id, { server_storage: labelCollectionEnabled, remove_level_one_email_trackers: typeof profile.remove_level_one_email_trackers === "boolean" ? trackerRemovalEnabled : undefined, store_phone_log: phoneCallerSMSLogEnabled, }); // After having enabled new server-side data storage, upload the locally stored labels: if ( profileData.data?.[0].server_storage === false && labelCollectionEnabled === true ) { const uploadLocalLabel = (alias: AliasData) => { const localLabel = localLabels?.find( (localLabel) => localLabel.mask_type === alias.mask_type && localLabel.id === alias.id, ); if (typeof localLabel !== "undefined") { aliasData.update(alias, { description: localLabel.description, generated_for: localLabel.generated_for, used_on: localLabel.used_on, }); } }; aliasData.randomAliasData.data?.forEach(uploadLocalLabel); aliasData.customAliasData.data?.forEach(uploadLocalLabel); } toast(l10n.getString("success-settings-update"), { type: "success" }); if (profileData.data?.[0].server_storage !== labelCollectionEnabled) { // If the user has changed their preference w.r.t. server storage of address labels, // notify the add-on about it: addonData.sendEvent("serverStorageChange"); } } catch (_e) { toast(l10n.getString("error-settings-update"), { type: "error" }); } }; const contactUsLink = profile.has_premium ? ( <li> <a href={`${runtimeData.data.FXA_ORIGIN}/support/?utm_source=${ getRuntimeConfig().frontendOrigin }`} target="_blank" rel="noopener noreferrer" title={l10n.getString("nav-profile-contact-tooltip")} > <ContactIcon className={styles["menu-icon"]} alt="" /> {l10n.getString("settings-meta-contact-label")} <NewTabIcon /> </a> </li> ) : null; const labelCollectionPrivacySetting = ( <div className={styles.field}> <h2 className={styles["field-heading"]}> {l10n.getString("setting-label-collection-heading-v2")} </h2> <div className={styles["field-content"]}> <div className={styles["field-control"]}> <input type="checkbox" name="label-collection" id="label-collection" defaultChecked={profile.server_storage} onChange={(e) => setLabelCollectionEnabled(e.target.checked)} /> <label htmlFor="label-collection"> {l10n.getString("setting-label-collection-description-3")} </label> </div> {labelCollectionWarning} </div> </div> ); const copyApiKeyToClipboard: MouseEventHandler<HTMLButtonElement> = () => { navigator.clipboard.writeText(profile.api_token); apiKeyElementRef.current?.select(); setJustCopiedApiKey(true); setTimeout(() => setJustCopiedApiKey(false), 1000); }; const apiKeySetting = ( <div className={styles.field}> <h2 className={styles["field-heading"]}> <label htmlFor="api-key"> {l10n.getString("setting-label-api-key")} </label> </h2> <div className={`${styles["copy-api-key-content"]} ${styles["field-content"]}`} > <div className={styles["settings-api-key-wrapper"]}> <input id="api-key" ref={apiKeyElementRef} className={styles["copy-api-key-display"]} value={profile.api_token} size={profile.api_token.length} readOnly={true} /> <span className={styles["copy-controls"]}> <span className={styles["copy-button-wrapper"]}> <button type="button" className={styles["copy-button"]} title={l10n.getString("settings-button-copy")} onClick={copyApiKeyToClipboard} > <CopyIcon alt={l10n.getString("settings-button-copy")} className={styles["copy-icon"]} width={24} height={24} /> </button> <span aria-hidden={!justCopiedApiKey} className={`${styles["copied-confirmation"]} ${ justCopiedApiKey ? styles["is-shown"] : "" }`} > {l10n.getString("setting-api-key-copied")} </span> </span> </span> </div> <div className={styles["settings-api-key-copy"]}> {l10n.getString("settings-api-key-description")}{" "} <b>{l10n.getString("settings-api-key-description-bolded")}</b> </div> </div> </div> ); // To allow us to add this UI before the back-end is updated, we only show it // when the profiles API actually returns a property `remove_level_one_email_trackers`. // Once it does, the commit that introduced this comment can be reverted. const trackerRemovalSetting = typeof profile.remove_level_one_email_trackers === "boolean" && isFlagActive(runtimeData.data, "tracker_removal") ? ( <div className={styles.field}> <h2 className={styles["field-heading"]}> <span className={styles["field-heading-icon-wrapper"]}> <HideIcon alt="" /> {l10n.getString("setting-tracker-removal-heading")} </span> </h2> <div className={styles["field-content"]}> <div className={styles["field-control"]}> <input type="checkbox" name="tracker-removal" id="tracker-removal" defaultChecked={profile.remove_level_one_email_trackers} onChange={(e) => setTrackerRemovalEnabled(e.target.checked)} /> <label htmlFor="tracker-removal"> <p>{l10n.getString("setting-tracker-removal-description")}</p> <p>{l10n.getString("setting-tracker-removal-note")}</p> </label> </div> <div className={styles["field-warning"]}> <InfoTriangleIcon alt="" /> <p>{l10n.getString("setting-tracker-removal-warning-2")}</p> </div> </div> </div> ) : null; const phoneCallerSMSLogSetting = isPhonesAvailableInCountry( runtimeData.data, ) ? ( <div className={styles.field}> <h2 className={styles["field-heading"]}> <span className={styles["field-heading-icon-wrapper"]}> {l10n.getString("phone-settings-caller-sms-log")} </span> </h2> <div className={styles["field-content"]}> <div className={styles["field-control"]}> <input type="checkbox" name="caller-sms-log" id="caller-sms-log" defaultChecked={profile.store_phone_log} onChange={(e) => { setPhoneCallerSMSLogEnabled(e.target.checked); setPhoneCallerSMSLogEnabledLabel(!phoneCallerSMSLogEnabledLabel); }} /> <label htmlFor="caller-sms-log"> <p>{l10n.getString("phone-settings-caller-sms-log-description")}</p> </label> </div> {phoneCallerSMSLogEnabledLabel ? ( <div className={styles["field-warning"]}> <InfoTriangleIcon alt="" /> <p>{l10n.getString("phone-settings-caller-sms-log-warning")}</p> </div> ) : null} </div> </div> ) : null; return ( <> <Layout runtimeData={runtimeData.data}> <div className={styles["settings-page"]}> <main className={styles.main}> {currentSettingWarning} <div className={styles["settings-form-wrapper"]}> <form onSubmit={saveSettings} className={styles["settings-form"]}> {labelCollectionPrivacySetting} {apiKeySetting} {trackerRemovalSetting} {phoneCallerSMSLogSetting} <div className={styles.controls}> <Button type="submit"> {l10n.getString("settings-button-save-label")} </Button> </div> </form> </div> </main> <aside className={styles.menu}> <h1 className={styles.heading}> {l10n.getString("settings-headline")} </h1> <ul> {contactUsLink} <li> <a href={`${getRuntimeConfig().supportUrl}?utm_source=${ getRuntimeConfig().frontendOrigin }`} target="_blank" rel="noopener noreferrer" title={l10n.getString("settings-meta-help-tooltip")} > <SupportIcon className={styles["menu-icon"]} alt="" /> {l10n.getString("settings-meta-help-label")} <NewTabIcon /> </a> </li> <li> <a href="https://status.relay.firefox.com/" target="_blank" rel="noopener noreferrer" title={l10n.getString("settings-meta-status-tooltip")} > <PerformanceIcon className={styles["menu-icon"]} alt="" /> {l10n.getString("settings-meta-status-label")} <NewTabIcon /> </a> </li> </ul> </aside> </div> </Layout> </> ); }; export default Settings;