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

mozilla / fx-private-relay / 9899ed3f-bdd4-4fdc-ad80-a7d9b6cb5169

22 May 2024 10:56PM CUT coverage: 85.01% (+0.6%) from 84.41%
9899ed3f-bdd4-4fdc-ad80-a7d9b6cb5169

Pull #4721

circleci

jwhitlock
Fix spelling
Pull Request #4721: MPP-3439: Refactor DataIssueTask / CleanerTask, clean users with blank emails

3882 of 5008 branches covered (77.52%)

Branch coverage included in aggregate %.

730 of 730 new or added lines in 8 files covered. (100.0%)

13 existing lines in 2 files now uncovered.

15371 of 17640 relevant lines covered (87.14%)

10.51 hits per line

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

75.49
/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 QRCode from "react-qr-code";
1✔
34

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

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

64
  if (!profileData.isValidating && profileData.error) {
20!
UNCOV
65
    document.location.assign(getRuntimeConfig().fxaLoginUrl);
×
66
  }
67

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

73
  const profile = profileData.data[0];
20✔
74

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

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

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

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

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

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

143
  const contactUsLink = profile.has_premium ? (
20!
144
    <li>
145
      <a
146
        href={`${runtimeData.data.FXA_ORIGIN}/support/?utm_source=${
147
          getRuntimeConfig().frontendOrigin
148
        }`}
149
        target="_blank"
150
        rel="noopener noreferrer"
151
        title={l10n.getString("nav-profile-contact-tooltip")}
152
      >
153
        <ContactIcon className={styles["menu-icon"]} alt="" />
154
        {l10n.getString("settings-meta-contact-label")}
155
        <NewTabIcon />
156
      </a>
157
    </li>
158
  ) : null;
159

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

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

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

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

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

321
  return (
322
    <>
323
      <Layout runtimeData={runtimeData.data}>
324
        <div className={styles["settings-page"]}>
325
          <main className={styles.main}>
326
            {currentSettingWarning}
327
            <div className={styles["settings-form-wrapper"]}>
328
              <form onSubmit={saveSettings} className={styles["settings-form"]}>
329
                {labelCollectionPrivacySetting}
330
                {apiKeySetting}
331
                {trackerRemovalSetting}
332
                {phoneCallerSMSLogSetting}
333

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

382
export default Settings;
7✔
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