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

mozilla / fx-private-relay / cafee54a-4333-4df8-8efd-a806f6de7814

25 Sep 2025 04:40PM UTC coverage: 88.855% (-0.03%) from 88.88%
cafee54a-4333-4df8-8efd-a806f6de7814

Pull #5903

circleci

groovecoder
for MPP-4415 feat(ci): add workflow to build and publish API docs to GitHub Pages
Pull Request #5903: for MPP-4415 feat(ci): add workflow to build and publish API docs to GitHub Pages

2916 of 3923 branches covered (74.33%)

Branch coverage included in aggregate %.

18044 of 19666 relevant lines covered (91.75%)

11.42 hits per line

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

83.65
/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
    document.location.assign(getRuntimeConfig().fxaLoginUrl);
×
65
  }
66

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

72
  const profile = profileData.data[0];
28✔
73

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

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

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

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

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

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

142
  const contactUsLink = profile.has_premium ? (
28✔
143
    <li>
144
      <a
145
        href={`${runtimeData.data.FXA_ORIGIN}/support/?utm_source=${
146
          getRuntimeConfig().frontendOrigin
147
        }`}
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