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

mozilla / fx-private-relay / 093164e9-6aa6-4b7b-adb3-ec19388e9081

14 May 2025 02:50PM CUT coverage: 85.189% (-0.008%) from 85.197%
093164e9-6aa6-4b7b-adb3-ec19388e9081

Pull #5550

circleci

groovecoder
for MPP-3957: start update_fxrelay_allowlist_collection command
Pull Request #5550: for MPP-3957: start update_fxrelay_allowlist_collection command

2468 of 3609 branches covered (68.38%)

Branch coverage included in aggregate %.

77 of 101 new or added lines in 3 files covered. (76.24%)

23 existing lines in 3 files now uncovered.

17393 of 19705 relevant lines covered (88.27%)

9.64 hits per line

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

84.43
/frontend/src/components/dashboard/PremiumOnboarding.tsx
1
import { useState } from "react";
1✔
2
import { useOverlayTriggerState } from "react-stately";
1✔
3
import styles from "./PremiumOnboarding.module.scss";
1✔
4
import ManLaptopEmail from "./images/man-laptop-email-alt.svg";
1✔
5
import WomanOnCouch from "./images/woman-couch.svg";
1✔
6
import WomanEmail from "./images/woman-email.svg";
1✔
7
import { Button, LinkButton } from "../Button";
1✔
8
import Image from "../Image";
1✔
9
import { useGaViewPing } from "../../hooks/gaViewPing";
1✔
10
import { ProfileData } from "../../hooks/api/profile";
11
import { SubdomainSearchForm } from "./subdomain/SearchForm";
1✔
12
import { SubdomainConfirmationModal } from "./subdomain/ConfirmationModal";
1✔
13
import { getRuntimeConfig } from "../../config";
1✔
14
import { useMinViewportWidth } from "../../hooks/mediaQuery";
1✔
15
import { supportsChromeExtension } from "../../functions/userAgent";
1✔
16
import { CheckBadgeIcon, CheckIcon } from "../Icons";
1✔
17
import { useGaEvent } from "../../hooks/gaEvent";
1✔
18
import { useL10n } from "../../hooks/l10n";
1✔
19
import { VisuallyHidden } from "../VisuallyHidden";
1✔
20
import { Localized } from "../Localized";
1✔
21

22
export type Props = {
23
  profile: ProfileData;
24
  onNextStep: (step: number) => void;
25
  onPickSubdomain: (subdomain: string) => void;
26
};
27

28
/**
29
 * Shows the user how to take advantage of Premium features when they've just upgraded.
30
 */
31
export const PremiumOnboarding = (props: Props) => {
8✔
32
  const l10n = useL10n();
8✔
33
  const isLargeScreen = useMinViewportWidth("md");
8✔
34

35
  const getStartedButtonRef = useGaViewPing({
8✔
36
    category: "Premium Onboarding",
37
    label: "onboarding-step-1-continue",
38
    value: 1,
39
  });
40
  const skipDomainButtonRef = useGaViewPing({
8✔
41
    category: "Premium Onboarding",
42
    label: "onboarding-step-2-skip",
43
    value: 2,
44
  });
45
  const continueWithDomainButtonRef = useGaViewPing({
8✔
46
    category: "Premium Onboarding",
47
    label: "onboarding-step-2-continue",
48
    value: 2,
49
  });
50
  const skipAddonButtonRef = useGaViewPing({
8✔
51
    category: "Premium Onboarding",
52
    label: "onboarding-step-3-skip",
53
    value: 3,
54
  });
55
  const continueWithAddonButtonRef = useGaViewPing({
8✔
56
    category: "Premium Onboarding",
57
    label: "onboarding-step-3-continue",
58
    value: 3,
59
  });
60
  const gaEvent = useGaEvent();
8✔
61

62
  let step = null;
8✔
63
  let button = null;
8✔
64
  let skipButton = null;
8✔
65

66
  if (props.profile.onboarding_state === 0) {
8✔
67
    step = <StepOne />;
3✔
68

69
    const getStarted = () => {
3✔
UNCOV
70
      props.onNextStep(1);
×
71
      gaEvent({
×
72
        category: "Premium Onboarding",
73
        action: "Engage",
74
        label: "onboarding-step-1-continue",
75
        value: 1,
76
      });
77
    };
78

79
    button = (
80
      <Button ref={getStartedButtonRef} onClick={getStarted}>
81
        {l10n.getString("multi-part-onboarding-premium-welcome-feature-cta")}
82
      </Button>
83
    );
84
  }
85

86
  if (props.profile.onboarding_state === 1) {
8✔
87
    step = (
88
      <StepTwo
89
        onNextStep={props.onNextStep}
90
        profile={props.profile}
91
        onPickSubdomain={props.onPickSubdomain}
92
      />
93
    );
94

95
    if (typeof props.profile.subdomain !== "string") {
4✔
96
      const skipDomain = () => {
3✔
97
        props.onNextStep(2);
1✔
98
        gaEvent({
1✔
99
          category: "Premium Onboarding",
100
          action: "Engage",
101
          label: "onboarding-step-2-skip",
102
          value: 2,
103
        });
104
      };
105

106
      skipButton = (
107
        <button
108
          ref={skipDomainButtonRef}
109
          onClick={skipDomain}
110
          className={styles["skip-link"]}
111
        >
112
          {l10n.getString("multi-part-onboarding-skip")}
113
        </button>
114
      );
115
    } else {
116
      const getAddon = () => {
1✔
UNCOV
117
        props.onNextStep(2);
×
UNCOV
118
        gaEvent({
×
119
          category: "Premium Onboarding",
120
          action: "Engage",
121
          label: "onboarding-step-2-continue",
122
          value: 2,
123
        });
124
      };
125
      button = (
126
        <Button ref={continueWithDomainButtonRef} onClick={getAddon}>
127
          {l10n.getString("multi-part-onboarding-continue")}
128
        </Button>
129
      );
130
    }
131
  }
132

133
  if (props.profile.onboarding_state === 2) {
8✔
134
    step = <StepThree />;
1✔
135
    const { linkHref, linkMessageId } = getAddonDescriptionProps();
1✔
136

137
    const skipAddon = () => {
1✔
138
      props.onNextStep(3);
1✔
139
      gaEvent({
1✔
140
        category: "Premium Onboarding",
141
        action: "Engage",
142
        label: "onboarding-step-3-skip",
143
        value: 3,
144
      });
145
    };
146
    const goToDashboard = () => {
1✔
UNCOV
147
      props.onNextStep(3);
×
UNCOV
148
      gaEvent({
×
149
        category: "Premium Onboarding",
150
        action: "Engage",
151
        label: "onboarding-step-3-continue",
152
        value: 3,
153
      });
154
    };
155

156
    button = (
157
      <>
158
        <Button
159
          ref={continueWithAddonButtonRef}
160
          onClick={goToDashboard}
161
          className={`${styles["go-to-dashboard-button"]} ${
162
            isLargeScreen ? `is-visible-with-addon` : ""
1!
163
          }`}
164
        >
165
          {l10n.getString(
166
            "multi-part-onboarding-premium-extension-button-dashboard",
167
          )}
168
        </Button>
169
        <AddonDescriptionLinkButton
170
          linkHref={linkHref}
171
          linkMessageId={linkMessageId}
172
        />
173
      </>
174
    );
175

176
    skipButton = isLargeScreen ? (
1!
177
      <button
178
        className={`${styles["skip-link"]} is-hidden-with-addon`}
179
        ref={skipAddonButtonRef}
180
        onClick={skipAddon}
181
      >
182
        {l10n.getString("multi-part-onboarding-skip-download-extension")}
183
      </button>
184
    ) : null;
185
  }
186

187
  return (
188
    <>
189
      <section className={styles.onboarding}>
190
        {step}
191
        <div className={styles.controls}>
192
          {button}
193
          {/*
194
            Unfortunately <progress> is hard to style like we want, even though it expresses what we want.
195
            Thus, we render a <progress> for machines, and hide the styled elements for them.
196
            // TODO: Use react-aria's useProgressBar()?
197
          */}
198
          <VisuallyHidden>
199
            <progress
200
              max={getRuntimeConfig().maxOnboardingAvailable}
201
              value={props.profile.onboarding_state + 1}
202
            >
203
              {l10n.getString("multi-part-onboarding-step-counter", {
204
                step: props.profile.onboarding_state,
205
                max: getRuntimeConfig().maxOnboardingAvailable,
206
              })}
207
            </progress>
208
          </VisuallyHidden>
209
          <ol className={styles["styled-progress-bar"]} aria-hidden={true}>
210
            <li
211
              className={
212
                props.profile.onboarding_state >= 0
8!
213
                  ? styles["is-completed"]
214
                  : undefined
215
              }
216
            >
217
              <span></span>1
218
            </li>
219
            <li
220
              className={
221
                props.profile.onboarding_state >= 1
8✔
222
                  ? styles["is-completed"]
223
                  : undefined
224
              }
225
            >
226
              <span></span>2
227
            </li>
228
            <li
229
              className={
230
                props.profile.onboarding_state >= 2
8✔
231
                  ? styles["is-completed"]
232
                  : undefined
233
              }
234
            >
235
              <span></span>3
236
            </li>
237
          </ol>
238

239
          {skipButton}
240
        </div>
241
      </section>
242
    </>
243
  );
244
};
245

246
const StepOne = () => {
1✔
247
  const l10n = useL10n();
3✔
248

249
  type FeatureItemProps = {
250
    name: string;
251
  };
252

253
  const FeatureItem = (props: FeatureItemProps) => {
3✔
254
    return (
255
      <li>
256
        <CheckIcon alt={""} className={styles["check-icon"]} />
257
        <p>
258
          <strong>
259
            {l10n.getString(
260
              `multi-part-onboarding-premium-welcome-feature-headline-${props.name}`,
261
            )}
262
          </strong>
263
          <br />
264
          <span>
265
            {l10n.getString(
266
              `multi-part-onboarding-premium-welcome-feature-body-${props.name}`,
267
            )}
268
          </span>
269
        </p>
270
      </li>
271
    );
272
  };
273

274
  return (
275
    <div className={`${styles.step} ${styles["step-welcome"]}`}>
276
      <div className={styles["title-container"]}>
277
        <h2>
278
          {l10n.getString("multi-part-onboarding-premium-welcome-headline")}
279
        </h2>
280
        <p className={styles.lead}>
281
          {l10n.getString(
282
            "multi-part-onboarding-premium-welcome-subheadline-2",
283
          )}
284
        </p>
285
      </div>
286
      <div className={styles.description}>
287
        <Image src={WomanOnCouch} alt="" width={350} />
288
        <div>
289
          <span className={styles["description-caption"]}>
290
            {l10n.getString(
291
              "multi-part-onboarding-premium-welcome-feature-headline",
292
            )}
293
          </span>
294
          <br />
295
          <ul className={styles["feature-item-list"]}>
296
            <FeatureItem name={"unlimited-email-masks"} />
297
            <FeatureItem name={"create-masks-on-the-go"} />
298
            <FeatureItem name={"custom-inbox-controls"} />
299
            <FeatureItem name={"anonymous-replies"} />
300
          </ul>
301
        </div>
302
      </div>
303
    </div>
304
  );
305
};
306

307
type Step2Props = {
308
  profile: ProfileData;
309
  onPickSubdomain: (subdomain: string) => void;
310
  onNextStep: (step: number) => void;
311
};
312
const StepTwo = (props: Step2Props) => {
1✔
313
  const l10n = useL10n();
14✔
314
  const [showSubdomainConfirmation, setShowSubdomainConfirmation] =
315
    useState(false);
14✔
316

317
  const [chosenSubdomain, setChosenSubdomain] = useState("");
14✔
318
  const [partialSubdomain, setPartialSubdomain] = useState("");
14✔
319

320
  const modalState = useOverlayTriggerState({});
14✔
321

322
  const onPick = (subdomain: string) => {
14✔
323
    setChosenSubdomain(subdomain);
1✔
324
    modalState.open();
1✔
325
  };
326

327
  const onConfirm = () => {
14✔
UNCOV
328
    props.onPickSubdomain(chosenSubdomain);
×
UNCOV
329
    setShowSubdomainConfirmation(true);
×
330
  };
331

332
  const onType = (_partial: string) => {
14✔
333
    setPartialSubdomain(_partial);
9✔
334
  };
335

336
  // Opens the confirmation and success modal
337
  const dialog = modalState.isOpen ? (
14✔
338
    <SubdomainConfirmationModal
339
      subdomain={chosenSubdomain}
340
      isOpen={modalState.isOpen}
341
      isSet={typeof props.profile.subdomain === "string"}
342
      onClose={() => modalState.close()}
×
343
      onConfirm={onConfirm}
UNCOV
344
      onComplete={() => modalState.close()}
×
345
    />
346
  ) : null;
347

348
  // Switches between the custom domain search and display module
349
  const subdomain = showSubdomainConfirmation ? (
350
    <p className={styles["action-complete"]}>
×
351
      <span className={styles.label}>
352
        <CheckBadgeIcon alt="" width={18} height={18} />
353
        {l10n.getString("multi-part-onboarding-premium-email-domain-added")}
354
      </span>
355
      <div>@{props.profile.subdomain}</div>
356
      <span className={styles.domain}>.{getRuntimeConfig().mozmailDomain}</span>
357
    </p>
358
  ) : (
359
    <>
360
      <div className={styles["domain-example"]}>
361
        ***@
362
        <span className={styles["customizable-part"]}>
363
          {partialSubdomain !== ""
14✔
364
            ? partialSubdomain
365
            : l10n.getString(
366
                "multi-part-onboarding-premium-email-domain-placeholder",
367
              )}
368
        </span>
369
        .{getRuntimeConfig().mozmailDomain}
370
      </div>
371
      <SubdomainSearchForm onType={onType} onPick={onPick} />
372
    </>
373
  );
374

375
  return (
376
    <div className={`${styles.step} ${styles["step-custom-domain"]}`}>
377
      <div className={styles["title-container"]}>
378
        <h2>
379
          {l10n.getString(
380
            "multi-part-onboarding-premium-email-domain-headline",
381
          )}
382
        </h2>
383
      </div>
384
      <div className={styles.description}>
385
        <Image src={WomanEmail} alt="" width={400} />
386
        <div className={styles.content}>
387
          <p className={styles["subdomain-description"]}>
388
            <span className={styles["description-caption"]}>
389
              {l10n.getString(
390
                "multi-part-onboarding-premium-email-domain-feature-headline",
391
              )}
392
            </span>
393
            <span className={styles["description-bolded-headline"]}>
394
              {l10n.getString(
395
                "multi-part-onboarding-premium-email-domain-headline-create-masks-on-the-go",
396
              )}
397
            </span>
398
            <br />
399
            {!showSubdomainConfirmation ? (
14!
400
              <Localized
401
                id="multi-part-onboarding-premium-email-domain-feature-body"
402
                vars={{ mozmail: "mozmail.com" }}
403
                elems={{
404
                  p: <p />,
405
                }}
406
              >
407
                <span />
408
              </Localized>
409
            ) : null}
410
          </p>
411
          {subdomain}
412
          {dialog}
413
        </div>
414
      </div>
415
    </div>
416
  );
417
};
418

419
const StepThree = () => {
1✔
420
  const l10n = useL10n();
1✔
421

422
  return (
423
    <div className={`${styles.step} ${styles["step-addon"]}`}>
424
      <div className={styles["title-container"]}>
425
        <StepThreeTitle />
426
      </div>
427
      <div className={styles.description}>
428
        <Image src={ManLaptopEmail} alt="" width={500} />
429
        <div>
430
          <p className={styles["reply-description"]}>
431
            <span className={styles["description-caption"]}>
432
              {l10n.getString("onboarding-premium-title-detail")}
433
            </span>
434
            <br />
435
            <span className={styles["description-bolded-headline"]}>
436
              {l10n.getString(
437
                "multi-part-onboarding-premium-reply-description",
438
              )}
439
            </span>
440
            <br />
441
            {l10n.getString("onboarding-premium-reply-description-2")}
442
          </p>
443
          <AddonDescription />
444
          <div
445
            className={`${styles["addon-description"]} is-visible-with-addon`}
446
          >
447
            <div className={styles["action-complete"]}>
448
              <CheckBadgeIcon alt="" width={18} height={18} />
449
              {l10n.getString("multi-part-onboarding-premium-extension-added")}
450
            </div>
451
            <p className={`${styles["addon-description"]}`}>
452
              {l10n.getString(
453
                "multi-part-onboarding-premium-added-extension-body",
454
              )}
455
            </p>
456
          </div>
457
        </div>
458
      </div>
459
    </div>
460
  );
461
};
462

463
const StepThreeTitle = () => {
1✔
464
  const l10n = useL10n();
1✔
465
  const isLargeScreen = useMinViewportWidth("md");
1✔
466
  const stepThreeTitleString = isLargeScreen
1!
467
    ? l10n.getString("multi-part-onboarding-premium-add-extension-headline")
468
    : l10n.getString("multi-part-onboarding-reply-headline");
469

470
  return <h2>{stepThreeTitleString}</h2>;
471
};
472

473
interface AddonDescriptionProps {
474
  headerMessageId: string;
475
  paragraphMessageId: string;
476
  linkHref: string;
477
  linkMessageId: string;
478
}
479
const getAddonDescriptionProps = () => {
1✔
480
  const linkForBrowser = supportsChromeExtension()
2!
481
    ? "https://chrome.google.com/webstore/detail/firefox-relay/lknpoadjjkjcmjhbjpcljdednccbldeb?utm_source=fx-relay&utm_medium=onboarding&utm_campaign=install-addon"
482
    : "https://addons.mozilla.org/en-CA/firefox/addon/private-relay/";
483

484
  return {
2✔
485
    headerMessageId:
486
      "multi-part-onboarding-premium-add-extension-feature-headline-create-any-site",
487
    paragraphMessageId:
488
      "multi-part-onboarding-premium-add-extension-feature-body",
489
    linkHref: linkForBrowser,
490
    linkMessageId: "multi-part-onboarding-premium-add-extension-feature-cta",
491
  };
492
};
493

494
const AddonDescription = () => {
1✔
495
  const l10n = useL10n();
1✔
496

497
  const isLargeScreen = useMinViewportWidth("md");
1✔
498
  const { headerMessageId, paragraphMessageId } = getAddonDescriptionProps();
1✔
499
  if (!isLargeScreen) {
1!
UNCOV
500
    return null;
×
501
  }
502
  return (
503
    <>
504
      <div className={`${styles["addon-description"]} is-hidden-with-addon`}>
505
        <span className={styles["description-caption"]}>
506
          {l10n.getString(
507
            "multi-part-onboarding-premium-add-extension-feature-headline",
508
          )}
509
        </span>
510
        <AddonDescriptionHeader headerMessageId={headerMessageId} />
511
        <AddonDescriptionParagraph paragraphMessageId={paragraphMessageId} />
512
      </div>
513
    </>
514
  );
515
};
516

517
const AddonDescriptionHeader = ({
1✔
518
  headerMessageId,
519
}: Pick<AddonDescriptionProps, "headerMessageId">) => {
520
  const l10n = useL10n();
1✔
521
  return <h3>{l10n.getString(headerMessageId)}</h3>;
522
};
523

524
const AddonDescriptionParagraph = ({
1✔
525
  paragraphMessageId,
526
}: Pick<AddonDescriptionProps, "paragraphMessageId">) => {
527
  const l10n = useL10n();
1✔
528
  return <p>{l10n.getString(paragraphMessageId)}</p>;
529
};
530

531
const AddonDescriptionLinkButton = ({
1✔
532
  linkHref,
533
  linkMessageId,
534
}: Pick<AddonDescriptionProps, "linkHref" | "linkMessageId">) => {
535
  const l10n = useL10n();
1✔
536
  return (
537
    <LinkButton
538
      href={linkHref}
539
      target="_blank"
540
      className={`is-hidden-with-addon ${styles["get-addon-button"]}`}
541
    >
542
      {l10n.getString(linkMessageId)}
543
    </LinkButton>
544
  );
545
};
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