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

mozilla / fx-private-relay / 8b39734c-31ce-4dbf-9b05-171797c18cae

02 Oct 2025 04:03PM UTC coverage: 88.815% (-0.05%) from 88.863%
8b39734c-31ce-4dbf-9b05-171797c18cae

Pull #5924

circleci

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

2932 of 3953 branches covered (74.17%)

Branch coverage included in aggregate %.

13 of 21 new or added lines in 5 files covered. (61.9%)

4 existing lines in 3 files now uncovered.

18086 of 19712 relevant lines covered (91.75%)

11.52 hits per line

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

81.31
/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

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

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

63
  if (!profileData.isValidating && profileData.error) {
28!
64
    const authParams =
NEW
65
      typeof window !== "undefined"
×
66
        ? encodeURIComponent(window.location.search.replace(/^\?/, ""))
67
        : "";
NEW
68
    document.location.assign(
×
69
      `${getRuntimeConfig().fxaLoginUrl}&auth_params=${authParams}`,
70
    );
71
  }
72

73
  if (!profileData.data || !runtimeData.data) {
28!
74
    // TODO: Show a loading spinner?
75
    return null;
×
76
  }
77

78
  const profile = profileData.data[0];
28✔
79

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

100
  const saveSettings: FormEventHandler = async (event) => {
28✔
101
    event.preventDefault();
2✔
102

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

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

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

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

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

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

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

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

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

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

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

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

377
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