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

mozilla / fx-private-relay / d4d9f278-d845-4992-8c81-4f3757c427a1

08 Sep 2025 02:07PM UTC coverage: 86.303% (-1.8%) from 88.121%
d4d9f278-d845-4992-8c81-4f3757c427a1

Pull #5842

circleci

joeherm
fix(deploy): Update CircleCI to use common Dockerfile for building frontend
Pull Request #5842: fix(deploy): Unify Dockerfiles

2744 of 3951 branches covered (69.45%)

Branch coverage included in aggregate %.

17910 of 19981 relevant lines covered (89.64%)

9.96 hits per line

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

78.21
/frontend/src/components/dashboard/aliases/BlockLevelSlider.tsx
1
import { ReactLocalization } from "@fluent/react";
2
import { HTMLAttributes, ReactNode, useRef } from "react";
3✔
3
import {
4
  FocusScope,
5
  mergeProps,
6
  OverlayContainer,
7
  useButton,
8
  useDialog,
9
  useFocusRing,
10
  useModal,
11
  useOverlay,
12
  useOverlayPosition,
13
  useOverlayTrigger,
14
  useSlider,
15
  useSliderThumb,
16
} from "react-aria";
3✔
17
import Link from "next/link";
3✔
18
import {
19
  SliderState,
20
  useOverlayTriggerState,
21
  useSliderState,
22
} from "react-stately";
3✔
23
import styles from "./BlockLevelSlider.module.scss";
3✔
24
import UmbrellaClosed from "./images/umbrella-closed.svg";
3✔
25
import UmbrellaClosedMobile from "./images/umbrella-closed-mobile.svg";
3✔
26
import UmbrellaSemi from "./images/umbrella-semi.svg";
3✔
27
import UmbrellaSemiMobile from "./images/umbrella-semi-mobile.svg";
3✔
28
import UmbrellaOpen from "./images/umbrella-open.svg";
3✔
29
import UmbrellaOpenMobile from "./images/umbrella-open-mobile.svg";
3✔
30
import Image from "../../Image";
3✔
31
import { AliasData } from "../../../hooks/api/aliases";
32
import { CloseIcon, LockIcon } from "../../Icons";
3✔
33
import { useGaEvent } from "../../../hooks/gaEvent";
3✔
34
import { useL10n } from "../../../hooks/l10n";
3✔
35
import { VisuallyHidden } from "../../VisuallyHidden";
3✔
36

37
export type BlockLevel = "none" | "promotional" | "all";
38
export type Props = {
39
  alias: AliasData;
40
  onChange: (blockLevel: BlockLevel) => void;
41
  hasPremium: boolean;
42
  premiumAvailableInCountry: boolean;
43
};
44

45
/**
46
 * The slider has only a single thumb, so we can always refer to it by its index of 0.
47
 */
48
const onlyThumbIndex = 0;
3✔
49

50
export const BlockLevelSlider = (props: Props) => {
140✔
51
  const l10n = useL10n();
143✔
52
  const trackRef = useRef<HTMLDivElement | null>(null);
143✔
53
  const gaEvent = useGaEvent();
143✔
54
  const numberFormatter = new SliderValueFormatter(l10n);
143✔
55
  const sliderSettings: Parameters<typeof useSliderState>[0] = {
143✔
56
    minValue: 0,
57
    maxValue: 100,
58
    step: props.hasPremium ? 50 : 100,
143✔
59
    numberFormatter: numberFormatter,
60
    label: l10n.getString("profile-promo-email-blocking-title"),
61
    onChange: (value) => {
62
      const blockLevel = getBlockLevelFromSliderValue(
1✔
63
        // The type assertion `as number[]` is here because react-stately now
64
        // also supports a slider having just a single value, added in
65
        // https://github.com/adobe/react-spectrum/pull/3264/commits/a2f570fb9e3ef74e706b9aa4441c84df799b29f1#diff-0e37708ee0e64cad9adcf7ddf87b9a7ae07dc77983925597394d5fb6a0a9d3a0R180-R181
66
        // (Whereas before, values were linked to the individual thumbs, which
67
        // is a bit weird when you just have a single thumb.)
68
        // However, since you still can't access that value directly on
69
        // `sliderState`, we'll still need `onlyThumbIndex` anyway, so for
70
        // consistency, we're still using a single-element array.
71
        (value as [number])[onlyThumbIndex],
72
      );
73
      gaEvent({
1✔
74
        category: "Dashboard Alias Settings",
75
        action: "Toggle Forwarding",
76
        label: getBlockLevelGaEventLabel(blockLevel),
77
      });
78
      // Free users can't enable Promotional email blocking:
79
      if (blockLevel !== "promotional" || props.hasPremium) {
1!
80
        return props.onChange(blockLevel);
1✔
81
      }
82
    },
83
    defaultValue: [getSliderValueForAlias(props.alias)],
84
  };
85
  const sliderState = useSliderState(sliderSettings);
143✔
86

87
  const { groupProps, trackProps, labelProps, outputProps } = useSlider(
143✔
88
    sliderSettings,
89
    sliderState,
90
    trackRef,
91
  );
92

93
  const lockIcon = props.hasPremium ? null : (
44✔
94
    <LockIcon alt="" width={14} height={16} className={styles["lock-icon"]} />
95
  );
96

97
  const premiumOnlyMarker = props.hasPremium ? null : (
44✔
98
    <>
99
      <br />
100
      <span className={styles["premium-only-marker"]}>
101
        {l10n.getString(
102
          "profile-promo-email-blocking-option-promotionals-premiumonly-marker",
103
        )}
104
      </span>
105
    </>
106
  );
107

108
  return (
109
    <div
110
      {...groupProps}
111
      className={`${styles.group} ${
112
        props.hasPremium ? styles["is-premium"] : styles["is-free"]
143✔
113
      }`}
114
    >
115
      <div className={styles.control}>
116
        <label {...labelProps} className={styles["slider-label"]}>
117
          {sliderSettings.label}
118
        </label>
119
        <div className={styles["track-wrapper"]}>
120
          <div {...trackProps} ref={trackRef} className={styles.track}>
121
            <div className={styles["track-line"]} />
122
            <div
123
              className={getTrackStopClassNames(
124
                props.alias,
125
                sliderState,
126
                "none",
127
              )}
128
            >
129
              <Image src={UmbrellaClosedMobile} alt="" />
130
              <p aria-hidden="true">{getLabelForBlockLevel("none", l10n)}</p>
131
            </div>
132
            {/*
133
              Only show the "Promotionals" track stop to Premium users.
134
              Free users will instead see a button that looks like it,
135
              but actually informs them of its only being available to Premium
136
              users (i.e. <PromotionalTrackStopGhost>)
137
             */}
138
            {props.hasPremium ? (
143✔
139
              <div
140
                className={getTrackStopClassNames(
141
                  props.alias,
142
                  sliderState,
143
                  "promotional",
144
                )}
145
              >
146
                <Image src={UmbrellaSemiMobile} alt="" />
147
                <p aria-hidden="true">
148
                  {getLabelForBlockLevel("promotional", l10n)}
149
                </p>
150
              </div>
151
            ) : null}
152
            <div
153
              className={getTrackStopClassNames(
154
                props.alias,
155
                sliderState,
156
                "all",
157
              )}
158
            >
159
              <Image src={UmbrellaOpenMobile} alt="" />
160
              <p aria-hidden="true">{getLabelForBlockLevel("all", l10n)}</p>
161
            </div>
162
            <Thumb sliderState={sliderState} trackRef={trackRef} />
163
          </div>
164
          {/*
165
            <PromotionalTrackStopGhost> is located outside of div.track,
166
            because for free users it has a tooltip that is not part of the
167
            track. If it were located inside it, clicking it would be
168
            interpreted as a click on the slider track.
169
            */}
170
          {!props.hasPremium ? (
143✔
171
            <PromotionalTrackStopGhost
172
              alias={props.alias}
173
              sliderState={sliderState}
174
              premiumAvailableInCountry={props.premiumAvailableInCountry}
175
            >
176
              <Image src={UmbrellaSemiMobile} alt="" />
177
              {lockIcon}
178
              <p>
179
                {getLabelForBlockLevel("promotional", l10n)}
180
                {premiumOnlyMarker}
181
              </p>
182
            </PromotionalTrackStopGhost>
183
          ) : null}
184
        </div>
185
      </div>
186
      <VisuallyHidden>
187
        {/* The p[aria-hidden] elements above already show the current and
188
        possible values for sighted users, but this element announces the
189
        current value for screen reader users. */}
190
        <output {...outputProps} className={styles["value-label"]}>
191
          {sliderState.getThumbValueLabel(onlyThumbIndex)}
192
        </output>
193
      </VisuallyHidden>
194
      <output {...outputProps} className={styles["value-description"]}>
195
        <BlockLevelIllustration
196
          level={getBlockLevelFromSliderValue(
197
            sliderState.getThumbValue(onlyThumbIndex),
198
          )}
199
        />
200
        <BlockLevelDescription
201
          level={getBlockLevelFromSliderValue(
202
            sliderState.getThumbValue(onlyThumbIndex),
203
          )}
204
        />
205
      </output>
206
    </div>
207
  );
208
};
209

210
type ThumbProps = {
211
  sliderState: SliderState;
212
  trackRef: React.RefObject<HTMLDivElement | null>;
213
};
214
const Thumb = (props: ThumbProps) => {
3✔
215
  const inputRef = useRef<HTMLInputElement>(null);
144✔
216
  const { thumbProps, inputProps } = useSliderThumb(
144✔
217
    {
218
      index: onlyThumbIndex,
219
      trackRef: props.trackRef,
220
      inputRef: inputRef,
221
    },
222
    props.sliderState,
223
  );
224

225
  const { focusProps, isFocusVisible } = useFocusRing();
144✔
226

227
  const focusClassName = isFocusVisible ? styles["is-focused"] : "";
144!
228
  const draggingClassName = props.sliderState.isThumbDragging(onlyThumbIndex)
144✔
229
    ? styles["is-dragging"]
230
    : "";
231

232
  return (
233
    <div
234
      className={styles["thumb-container"]}
235
      style={{
236
        left: `${props.sliderState.getThumbPercent(onlyThumbIndex) * 100}%`,
237
      }}
238
    >
239
      <div
240
        {...thumbProps}
241
        className={`${styles.thumb} ${focusClassName} ${draggingClassName}`}
242
      >
243
        <VisuallyHidden>
244
          <input ref={inputRef} {...mergeProps(inputProps, focusProps)} />
245
        </VisuallyHidden>
246
      </div>
247
    </div>
248
  );
249
};
250

251
const BlockLevelDescription = (props: { level: BlockLevel }) => {
3✔
252
  const l10n = useL10n();
143✔
253

254
  if (props.level === "none") {
143✔
255
    return (
256
      <span className={styles["value-description-content"]}>
257
        {l10n.getString("profile-promo-email-blocking-description-none-2")}
258
      </span>
259
    );
260
  }
261

262
  if (props.level === "promotional") {
3!
263
    return (
264
      <span className={styles["value-description-content"]}>
265
        {l10n.getString(
266
          "profile-promo-email-blocking-description-promotionals",
267
        )}
268
        <Link href="/faq#faq-promotional-email-blocking">
269
          {l10n.getString("banner-label-data-notification-body-cta")}
270
        </Link>
271
      </span>
272
    );
273
  }
274

275
  return (
276
    <span className={styles["value-description-content"]}>
277
      {l10n.getString("profile-promo-email-blocking-description-all-2")}
278
    </span>
279
  );
280
};
281
const BlockLevelIllustration = (props: { level: BlockLevel }) => {
3✔
282
  if (props.level === "none") {
143✔
283
    return <Image src={UmbrellaClosed} height={UmbrellaClosed.height} alt="" />;
284
  }
285

286
  if (props.level === "promotional") {
3!
287
    return <Image src={UmbrellaSemi} height={UmbrellaSemi.height} alt="" />;
288
  }
289

290
  return <Image src={UmbrellaOpen} height={UmbrellaOpen.height} alt="" />;
291
};
292

293
type PromotionalTrackStopGhostProps = {
294
  alias: AliasData;
295
  sliderState: SliderState;
296
  premiumAvailableInCountry: boolean;
297
  children: ReactNode;
298
};
299
/**
300
 * Pretends to be the regular track stop to enable promotional email blocking,
301
 * but is actually a button that triggers a tooltip displaying that promotional
302
 * email blocking is only available to Premium users.
303
 */
304
const PromotionalTrackStopGhost = (props: PromotionalTrackStopGhostProps) => {
3✔
305
  const overlayTriggerState = useOverlayTriggerState({});
99✔
306
  const triggerRef = useRef<HTMLButtonElement | null>(null);
99✔
307

308
  const { triggerProps, overlayProps } = useOverlayTrigger(
99✔
309
    {
310
      type: "dialog",
311
    },
312
    overlayTriggerState,
313
    triggerRef as React.RefObject<HTMLButtonElement>,
314
  );
315
  const { buttonProps } = useButton(triggerProps, triggerRef);
99✔
316

317
  return (
318
    <span className={styles.wrapper}>
319
      <button
320
        {...buttonProps}
321
        ref={triggerRef}
322
        type="button"
323
        className={styles["promotional-ghost-track-stop"]}
324
      >
325
        <span
326
          className={`${styles["track-stop"]} ${
327
            styles["track-stop-promotional"]
328
          } ${overlayTriggerState.isOpen ? styles["is-selected"] : ""}`}
99!
329
        >
330
          {props.children}
331
        </span>
332
      </button>
333
      {overlayTriggerState.isOpen && (
99✔
334
        <OverlayContainer>
335
          <PromotionalTooltip
336
            onClose={overlayTriggerState.close}
337
            triggerRef={triggerRef}
338
            overlayProps={overlayProps}
339
            premiumAvailableInCountry={props.premiumAvailableInCountry}
340
          />
341
        </OverlayContainer>
342
      )}
343
    </span>
344
  );
345
};
346

347
type PromotionalTooltipProps = {
348
  onClose: () => void;
349
  triggerRef: React.RefObject<HTMLButtonElement | null>;
350
  overlayProps: HTMLAttributes<HTMLDivElement>;
351
  premiumAvailableInCountry: boolean;
352
};
353
const PromotionalTooltip = (props: PromotionalTooltipProps) => {
3✔
354
  const l10n = useL10n();
×
355
  const overlayRef = useRef<HTMLDivElement>(null);
×
356
  const { overlayProps, underlayProps } = useOverlay(
×
357
    { isOpen: true, onClose: props.onClose, isDismissable: true },
358
    overlayRef,
359
  );
360

361
  const { modalProps } = useModal();
×
362

363
  const { dialogProps, titleProps } = useDialog({}, overlayRef);
×
364

365
  const overlayPositionProps = useOverlayPosition({
×
366
    targetRef: props.triggerRef,
367
    overlayRef: overlayRef,
368
    placement: "bottom left",
369
    offset: 10,
370
  }).overlayProps;
371

372
  const closeButtonRef = useRef<HTMLButtonElement>(null);
×
373
  const closeButtonProps = useButton(
×
374
    {
375
      onPress: () => props.onClose(),
×
376
    },
377
    closeButtonRef,
378
  ).buttonProps;
379

380
  const link = props.premiumAvailableInCountry ? (
381
    <Link href="/premium/">
×
382
      {l10n.getString(
383
        "profile-promo-email-blocking-description-promotionals-locked-cta",
384
      )}
385
    </Link>
386
  ) : (
387
    <Link href="/premium/waitlist">
388
      {l10n.getString(
389
        "profile-promo-email-blocking-description-promotionals-locked-waitlist-cta",
390
      )}
391
    </Link>
392
  );
393

394
  return (
395
    <div {...underlayProps} className={styles["upgrade-tooltip-underlay"]}>
396
      <FocusScope restoreFocus contain autoFocus>
397
        <div
398
          {...mergeProps(
399
            overlayProps,
400
            dialogProps,
401
            props.overlayProps,
402
            overlayPositionProps,
403
            modalProps,
404
          )}
405
          ref={overlayRef}
406
          className={styles["upgrade-tooltip"]}
407
        >
408
          <Image
409
            className={styles["promotionals-blocking-icon"]}
410
            src={UmbrellaSemi}
411
            alt=""
412
          />
413
          <span className={styles["upgrade-message"]}>
414
            <b className={styles["locked-message"]} {...titleProps}>
415
              <LockIcon alt="" className={styles["lock-icon"]} />
416
              {l10n.getString(
417
                "profile-promo-email-blocking-description-promotionals-locked-label",
418
              )}
419
            </b>
420
            {l10n.getString(
421
              "profile-promo-email-blocking-description-promotionals",
422
            )}
423
            {link}
424
          </span>
425
          <button
426
            {...closeButtonProps}
427
            ref={closeButtonRef}
428
            className={styles["close-button"]}
429
          >
430
            <CloseIcon
431
              alt={l10n.getString(
432
                "profile-promo-email-blocking-description-promotionals-locked-close",
433
              )}
434
            />
435
          </button>
436
        </div>
437
      </FocusScope>
438
    </div>
439
  );
440
};
441

442
function getSliderValueForAlias(alias: AliasData): number {
443
  if (alias.enabled === false) {
143!
444
    return 100;
×
445
  }
446
  if (alias.block_list_emails === true) {
143!
447
    return 50;
×
448
  }
449
  return 0;
143✔
450
}
451

452
function getBlockLevelFromSliderValue(value: number): BlockLevel {
453
  if (value === 0) {
904✔
454
    return "none";
884✔
455
  }
456
  if (value === 50) {
20!
457
    return "promotional";
×
458
  }
459
  return "all";
20✔
460
}
461
function getLabelForBlockLevel(
462
  blockLevel: BlockLevel,
463
  l10n: ReactLocalization,
464
): string {
465
  switch (blockLevel) {
716✔
466
    case "none":
467
      return l10n.getString("profile-promo-email-blocking-option-none");
423✔
468
    case "promotional":
469
      return l10n.getString("profile-promo-email-blocking-option-promotions");
143✔
470
    case "all":
471
      return l10n.getString("profile-promo-email-blocking-option-all");
150✔
472
  }
473
}
474
class SliderValueFormatter implements Intl.NumberFormat {
475
  l10n: ReactLocalization;
476

477
  constructor(l10n: ReactLocalization) {
478
    this.l10n = l10n;
143✔
479
  }
480
  // This method is only implemented to conform with the `Intl.NumberFormat`
481
  // interface, but react-aria should only call the `.format` method:
482
  resolvedOptions(): Intl.ResolvedNumberFormatOptions {
483
    throw new Error("Method not implemented.");
×
484
  }
485
  // This method is only implemented to conform with the `Intl.NumberFormat`
486
  // interface, but react-aria should only call the `.format` method:
487
  formatToParts(_number?: number | bigint): Intl.NumberFormatPart[] {
488
    throw new Error("Method not implemented.");
×
489
  }
490
  // This method is only implemented to conform with the `Intl.NumberFormat`
491
  // interface, but react-aria should only call the `.format` method:
492
  formatRange(_startDate: number | bigint, _endDate: number | bigint): string {
493
    throw new Error("Method not implemented.");
×
494
  }
495
  // This method is only implemented to conform with the `Intl.NumberFormat`
496
  // interface, but react-aria should only call the `.format` method:
497
  formatRangeToParts(
498
    _startDate: number | bigint,
499
    _endDate: number | bigint,
500
  ): Intl.NumberFormatPart[] {
501
    throw new Error("Method not implemented.");
×
502
  }
503

504
  format(value: number): string {
505
    return getLabelForBlockLevel(
287✔
506
      getBlockLevelFromSliderValue(value),
507
      this.l10n,
508
    );
509
  }
510
}
511

512
function getBlockLevelGaEventLabel(blockLevel: BlockLevel): string {
513
  switch (blockLevel) {
1!
514
    case "none":
515
      return "User enabled forwarding";
×
516
    case "promotional":
517
      return "User enabled promotional emails blocking";
×
518
    case "all":
519
      return "User disabled forwarding";
1✔
520
  }
521
}
522

523
const isBlockLevelActive = (
3✔
524
  alias: AliasData,
525
  blockLevel: BlockLevel,
526
): boolean => {
527
  if (
330✔
528
    blockLevel === "none" &&
616✔
529
    alias.enabled === true &&
530
    alias.block_list_emails !== true
531
  ) {
532
    return true;
143✔
533
  }
534
  if (
187!
535
    blockLevel === "promotional" &&
275✔
536
    alias.enabled === true &&
537
    alias.block_list_emails === true
538
  ) {
539
    return true;
×
540
  }
541
  if (blockLevel === "all" && alias.enabled === false) {
187!
542
    return true;
×
543
  }
544
  return false;
187✔
545
};
546
const getTrackStopClassNames = (
3✔
547
  alias: AliasData,
548
  sliderState: SliderState,
549
  blockLevel: BlockLevel,
550
): string => {
551
  const isActiveClass = isBlockLevelActive(alias, blockLevel)
330✔
552
    ? styles["is-active"]
553
    : "";
554
  const isSelectedClass =
555
    getBlockLevelFromSliderValue(sliderState.getThumbValue(onlyThumbIndex)) ===
330✔
556
    blockLevel
557
      ? styles["is-selected"]
558
      : "";
559
  const blockLevelClassName = styles[`track-stop-${blockLevel}`];
330✔
560

561
  return `${styles["track-stop"]} ${blockLevelClassName} ${isActiveClass} ${isSelectedClass}`;
330✔
562
};
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