• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / f7bc8e83-3560-4369-be54-50d9795958aa

06 Oct 2025 01:05PM UTC coverage: 88.879% (+0.02%) from 88.863%
f7bc8e83-3560-4369-be54-50d9795958aa

Pull #5924

circleci

web-flow
Merge pull request #5941 from mozilla/MPP-4348-propagate-utm-params-hydration

Fix hydration errors
Pull Request #5924: MPP-4348: propagate UTM params from landing page to SubPlat and Accounts URLs

2925 of 3933 branches covered (74.37%)

Branch coverage included in aggregate %.

37 of 43 new or added lines in 12 files covered. (86.05%)

1 existing line in 1 file now uncovered.

18110 of 19734 relevant lines covered (91.77%)

13.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.96
/frontend/src/pages/accounts/settings.page.tsx
1
import type { NextPage } from "next";
2
import {
3
  FormEventHandler,
4
  MouseEventHandler,
5
  useEffect,
6
  useReducer,
7
  useRef,
8
  useState,
9
} from "react";
1✔
10
import { toast } from "react-toastify";
1✔
11
import styles from "./settings.module.scss";
1✔
12
import { Layout } from "../../components/layout/Layout";
1✔
13
import { Banner } from "../../components/Banner";
1✔
14
import { useProfiles } from "../../hooks/api/profile";
1✔
15
import {
16
  InfoTriangleIcon,
17
  HideIcon,
18
  NewTabIcon,
19
  PerformanceIcon,
20
  CopyIcon,
21
  SupportIcon,
22
  ContactIcon,
23
} from "../../components/Icons";
1✔
24
import { Button } from "../../components/Button";
1✔
25
import { getRuntimeConfig } from "../../config";
1✔
26
import { useLocalLabels } from "../../hooks/localLabels";
1✔
27
import { AliasData, useAliases } from "../../hooks/api/aliases";
1✔
28
import { useRuntimeData } from "../../hooks/api/runtimeData";
1✔
29
import { useAddonData } from "../../hooks/addon";
1✔
30
import { isFlagActive } from "../../functions/waffle";
1✔
31
import { isPhonesAvailableInCountry } from "../../functions/getPlan";
1✔
32
import { useL10n } from "../../hooks/l10n";
1✔
33
import { useUtmApplier } from "../../hooks/utmApplier";
1✔
34

35
const Settings: NextPage = () => {
1✔
36
  const runtimeData = useRuntimeData();
28✔
37
  const profileData = useProfiles();
28✔
38
  const l10n = useL10n();
28✔
39
  const [localLabels] = useLocalLabels();
28✔
40
  const aliasData = useAliases();
28✔
41
  const addonData = useAddonData();
28✔
42
  const [labelCollectionEnabled, setLabelCollectionEnabled] = useState(
28✔
43
    profileData.data?.[0].server_storage,
44
  );
45
  const [trackerRemovalEnabled, setTrackerRemovalEnabled] = useState(
28✔
46
    profileData.data?.[0].remove_level_one_email_trackers,
47
  );
48
  const [phoneCallerSMSLogEnabled, setPhoneCallerSMSLogEnabled] = useState(
28✔
49
    profileData.data?.[0].store_phone_log,
50
  );
51
  const [phoneCallerSMSLogEnabledLabel, setPhoneCallerSMSLogEnabledLabel] =
52
    useState(false);
28✔
53
  const [justCopiedApiKey, setJustCopiedApiKey] = useState(false);
28✔
54
  const apiKeyElementRef = useRef<HTMLInputElement>(null);
28✔
55

56
  const [
57
    labelCollectionDisabledWarningToggles,
58
    countLabelCollectionDisabledWarningToggle,
59
  ] = useReducer((c) => c + 1, 0);
28✔
60
  useEffect(() => {
28✔
61
    countLabelCollectionDisabledWarningToggle();
13✔
62
  }, [labelCollectionEnabled]);
63

64
  const applyUtmParams = useUtmApplier();
28✔
65
  if (!profileData.isValidating && profileData.error) {
28!
NEW
66
    document.location.assign(applyUtmParams(getRuntimeConfig().fxaLoginUrl));
×
67
  }
68

69
  if (!profileData.data || !runtimeData.data) {
28!
70
    // TODO: Show a loading spinner?
71
    return null;
×
72
  }
73

74
  const profile = profileData.data[0];
28✔
75

76
  const currentSettingWarning = profile.server_storage ? null : (
17✔
77
    <div className={styles["banner-wrapper"]}>
78
      <Banner
79
        title={l10n.getString("settings-warning-collection-off-heading-3")}
80
        type="warning"
81
      >
82
        {l10n.getString("settings-warning-collection-off-description-3")}
83
      </Banner>
84
    </div>
85
  );
86
  // This warning should only be shown when data collection is explicitly toggled off,
87
  // i.e. not when it is off on page load.
88
  const labelCollectionWarning =
89
    labelCollectionDisabledWarningToggles > 1 && !labelCollectionEnabled ? (
28✔
90
      <div role="alert" className={styles["field-warning"]}>
91
        <InfoTriangleIcon alt="" />
92
        <p>{l10n.getString("setting-label-collection-off-warning-3")}</p>
93
      </div>
94
    ) : null;
95

96
  const saveSettings: FormEventHandler = async (event) => {
28✔
97
    event.preventDefault();
2✔
98

99
    try {
2✔
100
      await profileData.update(profile.id, {
2✔
101
        server_storage: labelCollectionEnabled,
102
        remove_level_one_email_trackers:
103
          typeof profile.remove_level_one_email_trackers === "boolean"
2!
104
            ? trackerRemovalEnabled
105
            : undefined,
106
        store_phone_log: phoneCallerSMSLogEnabled,
107
      });
108

109
      // After having enabled new server-side data storage, upload the locally stored labels:
110
      if (
2✔
111
        profileData.data?.[0].server_storage === false &&
3✔
112
        labelCollectionEnabled === true
113
      ) {
114
        const uploadLocalLabel = (alias: AliasData) => {
1✔
115
          const localLabel = localLabels?.find(
2✔
116
            (localLabel) =>
117
              localLabel.mask_type === alias.mask_type &&
×
118
              localLabel.id === alias.id,
119
          );
120
          if (typeof localLabel !== "undefined") {
2!
121
            aliasData.update(alias, {
×
122
              description: localLabel.description,
123
              generated_for: localLabel.generated_for,
124
              used_on: localLabel.used_on,
125
            });
126
          }
127
        };
128
        aliasData.randomAliasData.data?.forEach(uploadLocalLabel);
1✔
129
        aliasData.customAliasData.data?.forEach(uploadLocalLabel);
1✔
130
      }
131

132
      toast(l10n.getString("success-settings-update"), { type: "success" });
2✔
133

134
      if (profileData.data?.[0].server_storage !== labelCollectionEnabled) {
2!
135
        // If the user has changed their preference w.r.t. server storage of address labels,
136
        // notify the add-on about it:
137
        addonData.sendEvent("serverStorageChange");
2✔
138
      }
139
    } catch (_e) {
140
      toast(l10n.getString("error-settings-update"), { type: "error" });
×
141
    }
142
  };
143

144
  const contactUsLink = profile.has_premium ? (
28✔
145
    <li>
146
      <a
147
        href={"https://support.mozilla.org/questions/new/relay/form"}
148
        target="_blank"
149
        rel="noopener noreferrer"
150
        title={l10n.getString("nav-profile-contact-tooltip")}
151
      >
152
        <ContactIcon className={styles["menu-icon"]} alt="" />
153
        {l10n.getString("settings-meta-contact-label")}
154
        <NewTabIcon />
155
      </a>
156
    </li>
157
  ) : null;
158

159
  const labelCollectionPrivacySetting = (
160
    <div className={styles.field}>
161
      <h2 className={styles["field-heading"]}>
162
        {l10n.getString("setting-label-collection-heading-v2")}
163
      </h2>
164
      <div className={styles["field-content"]}>
165
        <div className={styles["field-control"]}>
166
          <input
167
            type="checkbox"
168
            name="label-collection"
169
            id="label-collection"
170
            defaultChecked={profile.server_storage}
171
            onChange={(e) => setLabelCollectionEnabled(e.target.checked)}
3✔
172
          />
173
          <label htmlFor="label-collection">
174
            {l10n.getString("setting-label-collection-description-3")}
175
          </label>
176
        </div>
177
        {labelCollectionWarning}
178
      </div>
179
    </div>
180
  );
181

182
  const copyApiKeyToClipboard: MouseEventHandler<HTMLButtonElement> = () => {
28✔
183
    navigator.clipboard.writeText(profile.api_token);
1✔
184
    apiKeyElementRef.current?.select();
1✔
185
    setJustCopiedApiKey(true);
1✔
186
    setTimeout(() => setJustCopiedApiKey(false), 1000);
1✔
187
  };
188

189
  const apiKeySetting = (
190
    <div className={styles.field}>
191
      <h2 className={styles["field-heading"]}>
192
        <label htmlFor="api-key">
193
          {l10n.getString("setting-label-api-key")}
194
        </label>
195
      </h2>
196
      <div
197
        className={`${styles["copy-api-key-content"]} ${styles["field-content"]}`}
198
      >
199
        <div className={styles["settings-api-key-wrapper"]}>
200
          <input
201
            id="api-key"
202
            ref={apiKeyElementRef}
203
            className={styles["copy-api-key-display"]}
204
            value={profile.api_token}
205
            size={profile.api_token.length}
206
            readOnly={true}
207
          />
208
          <span className={styles["copy-controls"]}>
209
            <span className={styles["copy-button-wrapper"]}>
210
              <button
211
                type="button"
212
                className={styles["copy-button"]}
213
                title={l10n.getString("settings-button-copy")}
214
                onClick={copyApiKeyToClipboard}
215
              >
216
                <CopyIcon
217
                  alt={l10n.getString("settings-button-copy")}
218
                  className={styles["copy-icon"]}
219
                  width={24}
220
                  height={24}
221
                />
222
              </button>
223
              <span
224
                aria-hidden={!justCopiedApiKey}
225
                className={`${styles["copied-confirmation"]} ${
226
                  justCopiedApiKey ? styles["is-shown"] : ""
28✔
227
                }`}
228
              >
229
                {l10n.getString("setting-api-key-copied")}
230
              </span>
231
            </span>
232
          </span>
233
        </div>
234
        <div className={styles["settings-api-key-copy"]}>
235
          {l10n.getString("settings-api-key-description")}{" "}
236
          <b>{l10n.getString("settings-api-key-description-bolded")}</b>
237
        </div>
238
      </div>
239
    </div>
240
  );
241

242
  // To allow us to add this UI before the back-end is updated, we only show it
243
  // when the profiles API actually returns a property `remove_level_one_email_trackers`.
244
  // Once it does, the commit that introduced this comment can be reverted.
245
  const trackerRemovalSetting =
246
    typeof profile.remove_level_one_email_trackers === "boolean" &&
28✔
247
    isFlagActive(runtimeData.data, "tracker_removal") ? (
248
      <div className={styles.field}>
249
        <h2 className={styles["field-heading"]}>
250
          <span className={styles["field-heading-icon-wrapper"]}>
251
            <HideIcon alt="" />
252
            {l10n.getString("setting-tracker-removal-heading")}
253
          </span>
254
        </h2>
255
        <div className={styles["field-content"]}>
256
          <div className={styles["field-control"]}>
257
            <input
258
              type="checkbox"
259
              name="tracker-removal"
260
              id="tracker-removal"
261
              defaultChecked={profile.remove_level_one_email_trackers}
262
              onChange={(e) => setTrackerRemovalEnabled(e.target.checked)}
1✔
263
            />
264
            <label htmlFor="tracker-removal">
265
              <p>{l10n.getString("setting-tracker-removal-description")}</p>
266
              <p>{l10n.getString("setting-tracker-removal-note")}</p>
267
            </label>
268
          </div>
269
          <div className={styles["field-warning"]}>
270
            <InfoTriangleIcon alt="" />
271
            <p>{l10n.getString("setting-tracker-removal-warning-2")}</p>
272
          </div>
273
        </div>
274
      </div>
275
    ) : null;
276

277
  const phoneCallerSMSLogSetting = isPhonesAvailableInCountry(
28!
278
    runtimeData.data,
279
  ) ? (
280
    <div className={styles.field}>
281
      <h2 className={styles["field-heading"]}>
282
        <span className={styles["field-heading-icon-wrapper"]}>
283
          {l10n.getString("phone-settings-caller-sms-log")}
284
        </span>
285
      </h2>
286
      <div className={styles["field-content"]}>
287
        <div className={styles["field-control"]}>
288
          <input
289
            type="checkbox"
290
            name="caller-sms-log"
291
            id="caller-sms-log"
292
            defaultChecked={profile.store_phone_log}
293
            onChange={(e) => {
294
              setPhoneCallerSMSLogEnabled(e.target.checked);
×
295
              setPhoneCallerSMSLogEnabledLabel(!phoneCallerSMSLogEnabledLabel);
×
296
            }}
297
          />
298
          <label htmlFor="caller-sms-log">
299
            <p>{l10n.getString("phone-settings-caller-sms-log-description")}</p>
300
          </label>
301
        </div>
302
        {phoneCallerSMSLogEnabledLabel ? (
×
303
          <div className={styles["field-warning"]}>
304
            <InfoTriangleIcon alt="" />
305
            <p>{l10n.getString("phone-settings-caller-sms-log-warning")}</p>
306
          </div>
307
        ) : null}
308
      </div>
309
    </div>
310
  ) : null;
311

312
  return (
313
    <>
314
      <Layout runtimeData={runtimeData.data}>
315
        <div className={styles["settings-page"]}>
316
          <main className={styles.main}>
317
            {currentSettingWarning}
318
            <div className={styles["settings-form-wrapper"]}>
319
              <form onSubmit={saveSettings} className={styles["settings-form"]}>
320
                {labelCollectionPrivacySetting}
321
                {apiKeySetting}
322
                {trackerRemovalSetting}
323
                {phoneCallerSMSLogSetting}
324

325
                <div className={styles.controls}>
326
                  <Button type="submit">
327
                    {l10n.getString("settings-button-save-label")}
328
                  </Button>
329
                </div>
330
              </form>
331
            </div>
332
          </main>
333
          <aside className={styles.menu}>
334
            <h1 className={styles.heading}>
335
              {l10n.getString("settings-headline")}
336
            </h1>
337
            <ul>
338
              {contactUsLink}
339
              <li>
340
                <a
341
                  href={`${getRuntimeConfig().supportUrl}?utm_source=${
342
                    getRuntimeConfig().frontendOrigin
343
                  }`}
344
                  target="_blank"
345
                  rel="noopener noreferrer"
346
                  title={l10n.getString("settings-meta-help-tooltip")}
347
                >
348
                  <SupportIcon className={styles["menu-icon"]} alt="" />
349
                  {l10n.getString("settings-meta-help-label")}
350
                  <NewTabIcon />
351
                </a>
352
              </li>
353
              <li>
354
                <a
355
                  href="https://status.relay.firefox.com/"
356
                  target="_blank"
357
                  rel="noopener noreferrer"
358
                  title={l10n.getString("settings-meta-status-tooltip")}
359
                >
360
                  <PerformanceIcon className={styles["menu-icon"]} alt="" />
361
                  {l10n.getString("settings-meta-status-label")}
362
                  <NewTabIcon />
363
                </a>
364
              </li>
365
            </ul>
366
          </aside>
367
        </div>
368
      </Layout>
369
    </>
370
  );
371
};
372

373
export default Settings;
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc