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

mozilla / fx-private-relay / 441e598b-d936-46c1-83a9-18fa6fcd1f35

13 May 2025 06:47PM CUT coverage: 85.197% (-0.04%) from 85.235%
441e598b-d936-46c1-83a9-18fa6fcd1f35

Pull #5549

circleci

vpremamozilla
iMPP-4192-extension-onboarding-step-removal
Pull Request #5549: MPP-4192 Extension Onboarding Step Removal

2460 of 3605 branches covered (68.24%)

Branch coverage included in aggregate %.

7 of 11 new or added lines in 2 files covered. (63.64%)

2 existing lines in 1 file now uncovered.

17316 of 19607 relevant lines covered (88.32%)

9.68 hits per line

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

80.92
/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 { supportsFirefoxExtension } 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
  const shouldShowStepThree = supportsFirefoxExtension();
8✔
35

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

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

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

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

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

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

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

107
      const skipToDashboard = () => {
3✔
108
        props.onNextStep(3);
1✔
109
        gaEvent({
1✔
110
          category: "Premium Onboarding",
111
          action: "Engage",
112
          label: "onboarding-step-3-skip",
113
          value: 3,
114
        });
115
      };
116

117
      skipButton = (
118
        <button
119
          ref={skipDomainButtonRef}
120
          onClick={shouldShowStepThree ? skipDomain : skipToDashboard}
3!
121
          className={styles["skip-link"]}
122
        >
123
          {l10n.getString("multi-part-onboarding-skip")}
124
        </button>
125
      );
126
    } else {
127
      const getAddon = () => {
1✔
128
        props.onNextStep(2);
×
129
        gaEvent({
×
130
          category: "Premium Onboarding",
131
          action: "Engage",
132
          label: "onboarding-step-2-continue",
133
          value: 2,
134
        });
135
      };
136
      button = (
137
        <Button ref={continueWithDomainButtonRef} onClick={getAddon}>
138
          {l10n.getString("multi-part-onboarding-continue")}
139
        </Button>
140
      );
141
    }
142
  }
143

144
  if (props.profile.onboarding_state === 2) {
8✔
145
    step = <StepThree />;
1✔
146
    const { linkHref, linkMessageId } = getAddonDescriptionProps();
1✔
147

148
    const skipAddon = () => {
1✔
149
      props.onNextStep(3);
1✔
150
      gaEvent({
1✔
151
        category: "Premium Onboarding",
152
        action: "Engage",
153
        label: "onboarding-step-3-skip",
154
        value: 3,
155
      });
156
    };
157
    const goToDashboard = () => {
1✔
158
      props.onNextStep(3);
×
159
      gaEvent({
×
160
        category: "Premium Onboarding",
161
        action: "Engage",
162
        label: "onboarding-step-3-continue",
163
        value: 3,
164
      });
165
    };
166

167
    button = (
168
      <>
169
        <Button
170
          ref={continueWithAddonButtonRef}
171
          onClick={goToDashboard}
172
          className={`${styles["go-to-dashboard-button"]} ${
173
            isLargeScreen ? `is-visible-with-addon` : ""
1!
174
          }`}
175
        >
176
          {l10n.getString(
177
            "multi-part-onboarding-premium-extension-button-dashboard",
178
          )}
179
        </Button>
180
        <AddonDescriptionLinkButton
181
          linkHref={linkHref}
182
          linkMessageId={linkMessageId}
183
        />
184
      </>
185
    );
186

187
    skipButton = isLargeScreen ? (
1!
188
      <button
189
        className={`${styles["skip-link"]} is-hidden-with-addon`}
190
        ref={skipAddonButtonRef}
191
        onClick={skipAddon}
192
      >
193
        {l10n.getString("multi-part-onboarding-skip-download-extension")}
194
      </button>
195
    ) : null;
196
  }
197

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

252
          {skipButton}
253
        </div>
254
      </section>
255
    </>
256
  );
257
};
258

259
const StepOne = () => {
1✔
260
  const l10n = useL10n();
3✔
261

262
  type FeatureItemProps = {
263
    name: string;
264
  };
265

266
  const FeatureItem = (props: FeatureItemProps) => {
3✔
267
    return (
268
      <li>
269
        <CheckIcon alt={""} className={styles["check-icon"]} />
270
        <p>
271
          <strong>
272
            {l10n.getString(
273
              `multi-part-onboarding-premium-welcome-feature-headline-${props.name}`,
274
            )}
275
          </strong>
276
          <br />
277
          <span>
278
            {l10n.getString(
279
              `multi-part-onboarding-premium-welcome-feature-body-${props.name}`,
280
            )}
281
          </span>
282
        </p>
283
      </li>
284
    );
285
  };
286

287
  return (
288
    <div className={`${styles.step} ${styles["step-welcome"]}`}>
289
      <div className={styles["title-container"]}>
290
        <h2>
291
          {l10n.getString("multi-part-onboarding-premium-welcome-headline")}
292
        </h2>
293
        <p className={styles.lead}>
294
          {l10n.getString(
295
            "multi-part-onboarding-premium-welcome-subheadline-2",
296
          )}
297
        </p>
298
      </div>
299
      <div className={styles.description}>
300
        <Image src={WomanOnCouch} alt="" width={350} />
301
        <div>
302
          <span className={styles["description-caption"]}>
303
            {l10n.getString(
304
              "multi-part-onboarding-premium-welcome-feature-headline",
305
            )}
306
          </span>
307
          <br />
308
          <ul className={styles["feature-item-list"]}>
309
            <FeatureItem name={"unlimited-email-masks"} />
310
            <FeatureItem name={"create-masks-on-the-go"} />
311
            <FeatureItem name={"custom-inbox-controls"} />
312
            <FeatureItem name={"anonymous-replies"} />
313
          </ul>
314
        </div>
315
      </div>
316
    </div>
317
  );
318
};
319

320
type Step2Props = {
321
  profile: ProfileData;
322
  onPickSubdomain: (subdomain: string) => void;
323
  onNextStep: (step: number) => void;
324
};
325
const StepTwo = (props: Step2Props) => {
1✔
326
  const l10n = useL10n();
14✔
327
  const [showSubdomainConfirmation, setShowSubdomainConfirmation] =
328
    useState(false);
14✔
329

330
  const [chosenSubdomain, setChosenSubdomain] = useState("");
14✔
331
  const [partialSubdomain, setPartialSubdomain] = useState("");
14✔
332

333
  const modalState = useOverlayTriggerState({});
14✔
334

335
  const onPick = (subdomain: string) => {
14✔
336
    setChosenSubdomain(subdomain);
1✔
337
    modalState.open();
1✔
338
  };
339

340
  const onConfirm = () => {
14✔
341
    props.onPickSubdomain(chosenSubdomain);
×
342
    setShowSubdomainConfirmation(true);
×
343
  };
344

345
  const onType = (_partial: string) => {
14✔
346
    setPartialSubdomain(_partial);
9✔
347
  };
348

349
  // Opens the confirmation and success modal
350
  const dialog = modalState.isOpen ? (
14✔
351
    <SubdomainConfirmationModal
352
      subdomain={chosenSubdomain}
353
      isOpen={modalState.isOpen}
354
      isSet={typeof props.profile.subdomain === "string"}
355
      onClose={() => modalState.close()}
×
356
      onConfirm={onConfirm}
357
      onComplete={() => modalState.close()}
×
358
    />
359
  ) : null;
360

361
  // Switches between the custom domain search and display module
362
  const subdomain = showSubdomainConfirmation ? (
363
    <p className={styles["action-complete"]}>
×
364
      <span className={styles.label}>
365
        <CheckBadgeIcon alt="" width={18} height={18} />
366
        {l10n.getString("multi-part-onboarding-premium-email-domain-added")}
367
      </span>
368
      <div>@{props.profile.subdomain}</div>
369
      <span className={styles.domain}>.{getRuntimeConfig().mozmailDomain}</span>
370
    </p>
371
  ) : (
372
    <>
373
      <div className={styles["domain-example"]}>
374
        ***@
375
        <span className={styles["customizable-part"]}>
376
          {partialSubdomain !== ""
14✔
377
            ? partialSubdomain
378
            : l10n.getString(
379
                "multi-part-onboarding-premium-email-domain-placeholder",
380
              )}
381
        </span>
382
        .{getRuntimeConfig().mozmailDomain}
383
      </div>
384
      <SubdomainSearchForm onType={onType} onPick={onPick} />
385
    </>
386
  );
387

388
  return (
389
    <div className={`${styles.step} ${styles["step-custom-domain"]}`}>
390
      <div className={styles["title-container"]}>
391
        <h2>
392
          {l10n.getString(
393
            "multi-part-onboarding-premium-email-domain-headline",
394
          )}
395
        </h2>
396
      </div>
397
      <div className={styles.description}>
398
        <Image src={WomanEmail} alt="" width={400} />
399
        <div className={styles.content}>
400
          <p className={styles["subdomain-description"]}>
401
            <span className={styles["description-caption"]}>
402
              {l10n.getString(
403
                "multi-part-onboarding-premium-email-domain-feature-headline",
404
              )}
405
            </span>
406
            <span className={styles["description-bolded-headline"]}>
407
              {l10n.getString(
408
                "multi-part-onboarding-premium-email-domain-headline-create-masks-on-the-go",
409
              )}
410
            </span>
411
            <br />
412
            {!showSubdomainConfirmation ? (
14!
413
              <Localized
414
                id="multi-part-onboarding-premium-email-domain-feature-body"
415
                vars={{ mozmail: "mozmail.com" }}
416
                elems={{
417
                  p: <p />,
418
                }}
419
              >
420
                <span />
421
              </Localized>
422
            ) : null}
423
          </p>
424
          {subdomain}
425
          {dialog}
426
        </div>
427
      </div>
428
    </div>
429
  );
430
};
431

432
const StepThree = () => {
1✔
433
  const l10n = useL10n();
1✔
434

435
  return (
436
    <div className={`${styles.step} ${styles["step-addon"]}`}>
437
      <div className={styles["title-container"]}>
438
        <StepThreeTitle />
439
      </div>
440
      <div className={styles.description}>
441
        <Image src={ManLaptopEmail} alt="" width={500} />
442
        <div>
443
          <p className={styles["reply-description"]}>
444
            <span className={styles["description-caption"]}>
445
              {l10n.getString("onboarding-premium-title-detail")}
446
            </span>
447
            <br />
448
            <span className={styles["description-bolded-headline"]}>
449
              {l10n.getString(
450
                "multi-part-onboarding-premium-reply-description",
451
              )}
452
            </span>
453
            <br />
454
            {l10n.getString("onboarding-premium-reply-description-2")}
455
          </p>
456
          <AddonDescription />
457
          <div
458
            className={`${styles["addon-description"]} is-visible-with-addon`}
459
          >
460
            <div className={styles["action-complete"]}>
461
              <CheckBadgeIcon alt="" width={18} height={18} />
462
              {l10n.getString("multi-part-onboarding-premium-extension-added")}
463
            </div>
464
            <p className={`${styles["addon-description"]}`}>
465
              {l10n.getString(
466
                "multi-part-onboarding-premium-added-extension-body",
467
              )}
468
            </p>
469
          </div>
470
        </div>
471
      </div>
472
    </div>
473
  );
474
};
475

476
const StepThreeTitle = () => {
1✔
477
  const l10n = useL10n();
1✔
478
  const isLargeScreen = useMinViewportWidth("md");
1✔
479
  const stepThreeTitleString = isLargeScreen
1!
480
    ? l10n.getString("multi-part-onboarding-premium-add-extension-headline")
481
    : l10n.getString("multi-part-onboarding-reply-headline");
482

483
  return <h2>{stepThreeTitleString}</h2>;
484
};
485

486
interface AddonDescriptionProps {
487
  headerMessageId: string;
488
  paragraphMessageId: string;
489
  linkHref: string;
490
  linkMessageId: string;
491
}
492
const getAddonDescriptionProps = () => {
1✔
493
  const linkForBrowser =
494
    "https://addons.mozilla.org/en-CA/firefox/addon/private-relay/";
2✔
495

496
  return {
2✔
497
    headerMessageId:
498
      "multi-part-onboarding-premium-add-extension-feature-headline-create-any-site",
499
    paragraphMessageId:
500
      "multi-part-onboarding-premium-add-extension-feature-body",
501
    linkHref: linkForBrowser,
502
    linkMessageId: "multi-part-onboarding-premium-add-extension-feature-cta",
503
  };
504
};
505

506
const AddonDescription = () => {
1✔
507
  const l10n = useL10n();
1✔
508

509
  const isLargeScreen = useMinViewportWidth("md");
1✔
510
  const { headerMessageId, paragraphMessageId } = getAddonDescriptionProps();
1✔
511
  if (!isLargeScreen) {
1!
512
    return null;
×
513
  }
514
  return (
515
    <>
516
      <div className={`${styles["addon-description"]} is-hidden-with-addon`}>
517
        <span className={styles["description-caption"]}>
518
          {l10n.getString(
519
            "multi-part-onboarding-premium-add-extension-feature-headline",
520
          )}
521
        </span>
522
        <AddonDescriptionHeader headerMessageId={headerMessageId} />
523
        <AddonDescriptionParagraph paragraphMessageId={paragraphMessageId} />
524
      </div>
525
    </>
526
  );
527
};
528

529
const AddonDescriptionHeader = ({
1✔
530
  headerMessageId,
531
}: Pick<AddonDescriptionProps, "headerMessageId">) => {
532
  const l10n = useL10n();
1✔
533
  return <h3>{l10n.getString(headerMessageId)}</h3>;
534
};
535

536
const AddonDescriptionParagraph = ({
1✔
537
  paragraphMessageId,
538
}: Pick<AddonDescriptionProps, "paragraphMessageId">) => {
539
  const l10n = useL10n();
1✔
540
  return <p>{l10n.getString(paragraphMessageId)}</p>;
541
};
542

543
const AddonDescriptionLinkButton = ({
1✔
544
  linkHref,
545
  linkMessageId,
546
}: Pick<AddonDescriptionProps, "linkHref" | "linkMessageId">) => {
547
  const l10n = useL10n();
1✔
548
  return (
549
    <LinkButton
550
      href={linkHref}
551
      target="_blank"
552
      className={`is-hidden-with-addon ${styles["get-addon-button"]}`}
553
    >
554
      {l10n.getString(linkMessageId)}
555
    </LinkButton>
556
  );
557
};
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