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

mozilla / fx-private-relay / 027a12c3-914b-4a0e-9c75-b8da713d4be8

14 May 2025 02:34PM CUT coverage: 85.197% (-0.05%) from 85.243%
027a12c3-914b-4a0e-9c75-b8da713d4be8

push

circleci

web-flow
Merge pull request #5549 from mozilla/MPP-4192-extension-onboarding-step-removal

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%)

3 existing lines in 2 files now uncovered.

17316 of 19607 relevant lines covered (88.32%)

9.69 hits per line

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

26.73
/frontend/src/components/dashboard/FreeOnboarding.tsx
1
import styles from "./FreeOnboarding.module.scss";
1✔
2
import { ProfileData } from "../../hooks/api/profile";
3
import { VisuallyHidden } from "../VisuallyHidden";
1✔
4
import { getRuntimeConfig } from "../../config";
1✔
5
import { useL10n } from "../../hooks/l10n";
1✔
6
import WomanEmail from "./images/woman-email.svg";
1✔
7
import CheckMark from "./images/welcome-to-relay-check.svg";
1✔
8
import Plus from "./images/welcome-to-relay-plus.svg";
1✔
9
import Congratulations from "./images/free-onboarding-congratulations.svg";
1✔
10
import VerticalArrow from "./images/free-onboarding-vertical-arrow.svg";
1✔
11
import Emails from "./images/free-onboarding-emails.svg";
1✔
12
import WorkingMan from "./images/free-onboarding-work-anywhere.svg";
1✔
13
import Extension from "./images/free-onboarding-relay-extension.svg";
1✔
14
import SmallArrow from "./images/free-onboarding-arrow.svg";
1✔
15
import LargeArrow from "./images/free-onboarding-arrow-large.svg";
1✔
16
import { Button, LinkButton } from "../Button";
1✔
17
import Image from "../Image";
1✔
18
import { useGaViewPing } from "../../hooks/gaViewPing";
1✔
19
import { useGaEvent } from "../../hooks/gaEvent";
1✔
20
import { AliasData } from "../../hooks/api/aliases";
21
import { UserData } from "../../hooks/api/user";
22
import { RuntimeData } from "../../hooks/api/runtimeData";
23
import { EmailForwardingModal } from "./EmailForwardingModal";
1✔
24
import { useState } from "react";
1✔
25
import { supportsFirefoxExtension } from "../../functions/userAgent";
1✔
26
import { CheckBadgeIcon, ChevronRightIcon } from "../Icons";
1✔
27
import { AliasList } from "./aliases/AliasList";
1✔
28

29
export type Props = {
30
  profile: ProfileData;
31
  onNextStep: (step: number) => void;
32
  onPickSubdomain: (subdomain: string) => void;
33
  generateNewMask: (options: { mask_type: "random" }) => Promise<void>;
34
  hasReachedFreeMaskLimit: boolean;
35
  aliases: AliasData[];
36
  user: UserData;
37
  runtimeData?: RuntimeData;
38
  onUpdate: (alias: AliasData, updatedFields: Partial<AliasData>) => void;
39
  hasAtleastOneMask: boolean;
40
};
41

42
/**
43
 * Shows the user how to take advantage of Premium features when they've just upgraded.
44
 */
45
export const FreeOnboarding = (props: Props) => {
1✔
46
  const l10n = useL10n();
×
47
  const [isModalOpen, setIsModalOpen] = useState(false);
×
48
  const gaEvent = useGaEvent();
×
49

50
  let step = null;
×
51
  let button = null;
×
52
  let skipButton = null;
×
53
  let next = null;
×
54

55
  // TODO: Add GA events - for view events and pings
56
  const skipStepOneButtonRef = useGaViewPing({
×
57
    category: "Free Onboarding",
58
    label: "free-onboarding-step-1-skip",
59
    value: 1,
60
  });
61

62
  const skipStepTwoButtonRef = useGaViewPing({
×
63
    category: "Free Onboarding",
64
    label: "free-onboarding-step-2-skip",
65
    value: 1,
66
  });
67

68
  const nextStepTwoButtonRef = useGaViewPing({
×
69
    category: "Free Onboarding",
70
    label: "free-onboarding-step-2-next",
71
    value: 1,
72
  });
73

74
  const skipStepThreeButtonRef = useGaViewPing({
×
75
    category: "Free Onboarding",
76
    label: "free-onboarding-step-3-skip",
77
    value: 1,
78
  });
79

80
  if (props.profile.onboarding_free_state === 0) {
×
81
    const skipMaskCreation = () => {
×
82
      props.onNextStep(3);
×
83
      gaEvent({
×
84
        category: "Free Onboarding",
85
        action: "Engage",
86
        label: "onboarding-step-1-skip",
87
        value: 1,
88
      });
89
    };
90

91
    const createNewMask = async () => {
×
92
      let moveToNextStep = true;
×
93

94
      try {
×
95
        await props.generateNewMask({ mask_type: "random" });
×
96
      } catch (_e) {
97
        // On error, we can only move to the next step if the user has atleast 1 mask.
98
        if (!props.hasAtleastOneMask) {
×
99
          moveToNextStep = false;
×
100
        }
101
      }
102

103
      if (moveToNextStep) {
×
104
        props.onNextStep(1);
×
105
        gaEvent({
×
106
          category: "Free Onboarding",
107
          action: "Engage",
108
          label: "onboarding-step-1-create-random-mask",
109
          value: 1,
110
        });
111
      }
112
    };
113

114
    step = <StepOne />;
×
115

116
    skipButton = (
117
      <button
118
        ref={skipStepOneButtonRef}
119
        className={styles["skip-link"]}
120
        onClick={skipMaskCreation}
121
      >
122
        {l10n.getString("profile-free-onboarding-skip-step")}
123
      </button>
124
    );
125

126
    button = (
127
      <Button className={styles["generate-new-mask"]} onClick={createNewMask}>
128
        {l10n.getString("profile-free-onboarding-welcome-generate-new-mask")}
129
        <Image src={Plus} alt="" />
130
      </Button>
131
    );
132
  }
133

134
  if (props.profile.onboarding_free_state === 1) {
×
135
    const skipMaskTesting = () => {
×
136
      props.onNextStep(3);
×
137
      gaEvent({
×
138
        category: "Free Onboarding",
139
        action: "Engage",
140
        label: "onboarding-step-2-skip",
141
        value: 1,
142
      });
143
    };
144

145
    const nextStep = () => {
×
146
      props.onNextStep(2);
×
147
      gaEvent({
×
148
        category: "Free Onboarding",
149
        action: "Engage",
150
        label: "onboarding-step-2-next",
151
        value: 1,
152
      });
153
    };
154

155
    const forwardedEmail = () => {
×
156
      props.onNextStep(2);
×
157
      gaEvent({
×
158
        category: "Free Onboarding",
159
        action: "Engage",
160
        label: "onboarding-step-2-continue",
161
        value: 1,
162
      });
163
    };
164

165
    step = (
166
      <StepTwo
167
        aliases={props.aliases}
168
        profile={props.profile}
169
        user={props.user}
170
        runtimeData={props.runtimeData}
171
        continue={forwardedEmail}
172
        isModalOpen={isModalOpen}
173
        setIsModalOpen={setIsModalOpen}
174
        onUpdate={props.onUpdate}
175
      />
176
    );
177

178
    next = (
179
      <button
180
        ref={nextStepTwoButtonRef}
181
        className={styles["next-link"]}
182
        onClick={nextStep}
183
      >
184
        {l10n.getString("profile-free-onboarding-next-step")}
185
        <ChevronRightIcon className={styles.chevron} width={16} alt="" />
186
      </button>
187
    );
188

189
    button = (
190
      <Button
191
        className={styles["generate-new-mask"]}
192
        onClick={() => {
193
          setIsModalOpen(true);
×
194
        }}
195
      >
196
        {l10n.getString(
197
          "profile-free-onboarding-copy-mask-how-forwarding-works",
198
        )}
199
      </Button>
200
    );
201

202
    skipButton = (
203
      <button
204
        ref={skipStepTwoButtonRef}
205
        className={styles["skip-link"]}
206
        onClick={skipMaskTesting}
207
      >
208
        {l10n.getString("profile-free-onboarding-skip-step")}
209
      </button>
210
    );
211
  }
212

213
  if (props.profile.onboarding_free_state === 2) {
×
214
    const linkForBrowser =
NEW
215
      "https://addons.mozilla.org/firefox/addon/private-relay/";
×
216

217
    const skipAddonStep = () => {
×
218
      props.onNextStep(3);
×
219
      gaEvent({
×
220
        category: "Free Onboarding",
221
        action: "Engage",
222
        label: "onboarding-step-3-skip",
223
        value: 1,
224
      });
225
    };
226

227
    const finish = () => {
×
228
      // this will trigger confetti in the dashboard
229
      props.onNextStep(4);
×
230
      gaEvent({
×
231
        category: "Free Onboarding",
232
        action: "Engage",
233
        label: "onboarding-step-3-complete",
234
        value: 1,
235
      });
236
    };
237

238
    step = <StepThree />;
×
239

NEW
240
    next = supportsFirefoxExtension() ? (
×
241
      <button
242
        ref={nextStepTwoButtonRef}
243
        className={styles["next-link"]}
244
        onClick={finish}
245
      >
246
        {l10n.getString("profile-free-onboarding-addon-finish")}
247
        <ChevronRightIcon className={styles.chevron} width={16} alt="" />
248
      </button>
249
    ) : null;
250

251
    button = supportsFirefoxExtension() ? (
252
      <>
×
253
        <LinkButton
254
          href={linkForBrowser}
255
          target="_blank"
256
          className={`is-hidden-with-addon ${styles["get-addon-button"]}`}
257
        >
258
          {l10n.getString("profile-free-onboarding-addon-get-extension")}
259
        </LinkButton>
260
        <div className={`${styles["addon-description"]} is-visible-with-addon`}>
261
          <div className={styles["action-complete"]}>
262
            <CheckBadgeIcon alt="" width={18} height={18} />
263
            {l10n.getString("multi-part-onboarding-premium-extension-added")}
264
          </div>
265
        </div>
266
      </>
267
    ) : (
268
      <LinkButton
269
        ref={nextStepTwoButtonRef}
270
        target="_blank"
271
        className={`is-hidden-with-addon ${styles["get-addon-button"]}`}
272
        onClick={finish}
273
      >
274
        {l10n.getString("profile-free-onboarding-addon-finish")}
275
      </LinkButton>
276
    );
277

NEW
278
    skipButton = supportsFirefoxExtension() ? (
×
279
      <button
280
        ref={skipStepThreeButtonRef}
281
        className={styles["skip-link"]}
282
        onClick={skipAddonStep}
283
      >
284
        {l10n.getString("profile-free-onboarding-skip-step")}
285
      </button>
286
    ) : null;
287
  }
288

289
  return (
290
    <section className={styles.onboarding}>
291
      {step}
292
      <div className={styles.controls}>
293
        {button}
294
        <div className={styles["progress-container"]}>
295
          <VisuallyHidden>
296
            <progress
297
              max={getRuntimeConfig().maxOnboardingAvailable}
298
              value={props.profile.onboarding_free_state + 1}
299
            >
300
              {l10n.getString("multi-part-onboarding-step-counter", {
301
                step: props.profile.onboarding_free_state,
302
                max: getRuntimeConfig().maxOnboardingAvailable,
303
              })}
304
            </progress>
305
          </VisuallyHidden>
306
          {next}
307
          <ol className={styles["styled-progress-bar"]} aria-hidden={true}>
308
            <li
309
              className={
310
                props.profile.onboarding_free_state >= 0
×
311
                  ? styles["is-completed"]
312
                  : undefined
313
              }
314
            >
315
              <span></span>1
316
            </li>
317
            <li
318
              className={
319
                props.profile.onboarding_free_state >= 1
×
320
                  ? styles["is-completed"]
321
                  : undefined
322
              }
323
            >
324
              <span></span>2
325
            </li>
326
            <li
327
              className={
328
                props.profile.onboarding_free_state >= 2
×
329
                  ? styles["is-completed"]
330
                  : undefined
331
              }
332
            >
333
              <span></span>3
334
            </li>
335
          </ol>
336
        </div>
337
        {skipButton}
338
      </div>
339
    </section>
340
  );
341
};
342

343
const StepOne = () => {
1✔
344
  const l10n = useL10n();
×
345

346
  return (
347
    <div className={`${styles.step} ${styles["step-welcome"]}`}>
348
      <div className={styles["welcome-header"]}>
349
        <h1>{l10n.getString("profile-free-onboarding-welcome-headline")}</h1>
350
        <p>{l10n.getString("profile-free-onboarding-welcome-description")}</p>
351
      </div>
352
      <div className={styles["content-wrapper"]}>
353
        <Image src={WomanEmail} alt="" width={475} />
354
        <div className={styles["content-text"]}>
355
          <div>
356
            <Image className={styles["hidden-mobile"]} src={CheckMark} alt="" />
357
            <p className={styles["headline"]}>
358
              {l10n.getString(
359
                "profile-free-onboarding-welcome-item-headline-1",
360
              )}
361
            </p>
362
            <p className={styles["description"]}>
363
              {l10n.getString(
364
                "profile-free-onboarding-welcome-item-description-1",
365
              )}
366
            </p>
367
          </div>
368
          <div>
369
            <p className={styles["headline"]}>
370
              {l10n.getString(
371
                "profile-free-onboarding-welcome-item-headline-2",
372
              )}
373
            </p>
374
            <p className={styles["description"]}>
375
              {l10n.getString(
376
                "profile-free-onboarding-welcome-item-description-2",
377
              )}
378
            </p>
379
          </div>
380
        </div>
381
      </div>
382
    </div>
383
  );
384
};
385

386
type StepTwoProps = {
387
  aliases: AliasData[];
388
  profile: ProfileData;
389
  user: UserData;
390
  runtimeData?: RuntimeData;
391
  isModalOpen: boolean;
392
  setIsModalOpen: (isOpen: boolean) => void;
393
  continue: () => void;
394
  onUpdate: (alias: AliasData, updatedFields: Partial<AliasData>) => void;
395
};
396

397
const StepTwo = (props: StepTwoProps) => {
1✔
398
  const l10n = useL10n();
×
399
  const [isSet, setIsSet] = useState(false);
×
400

401
  return (
402
    <div className={`${styles.step} ${styles["step-copy-mask"]}`}>
403
      <EmailForwardingModal
404
        isOpen={props.isModalOpen}
405
        onClose={() => {
406
          props.setIsModalOpen(false);
×
407
        }}
408
        onComplete={() => {
409
          props.setIsModalOpen(false);
×
410
          props.continue();
×
411
        }}
412
        onConfirm={() => {
413
          setIsSet(true);
×
414
        }}
415
        isSet={isSet}
416
      />
417
      <div className={styles["copy-mask-header"]}>
418
        <h1>{l10n.getString("profile-free-onboarding-copy-mask-headline")}</h1>
419
        <p>{l10n.getString("profile-free-onboarding-copy-mask-description")}</p>
420
      </div>
421
      <div className={styles["content-wrapper-copy-mask"]}>
422
        <div className={styles["copy-mask-arrow-element"]}>
423
          <Image src={VerticalArrow} alt="" />
424
        </div>
425
        <AliasList
426
          aliases={props.aliases}
427
          onCreate={() => {}}
428
          onUpdate={props.onUpdate}
429
          onDelete={() => {}}
430
          profile={props.profile}
431
          user={props.user}
432
          runtimeData={props.runtimeData}
433
          onboarding={true}
434
        >
435
          <div className={styles["content-wrapper-copy-mask-items"]}>
436
            <div className={styles["content-item"]}>
437
              <Image src={Emails} alt="" />
438
              <div className={styles["content-text"]}>
439
                <p className={styles["headline"]}>
440
                  {l10n.getString(
441
                    "profile-free-onboarding-copy-mask-item-headline-1",
442
                  )}
443
                </p>
444
                <p className={styles["description"]}>
445
                  {l10n.getString(
446
                    "profile-free-onboarding-copy-mask-item-description-1",
447
                  )}
448
                </p>
449
              </div>
450
            </div>
451
            <hr />
452
            <div className={styles["content-item"]}>
453
              <Image src={Congratulations} alt="" />
454
              <div className={styles["content-text"]}>
455
                <p className={styles["headline"]}>
456
                  {l10n.getString(
457
                    "profile-free-onboarding-copy-mask-item-headline-2",
458
                  )}
459
                </p>
460
                <p className={styles["description"]}>
461
                  {l10n.getString(
462
                    "profile-free-onboarding-copy-mask-item-description-2",
463
                  )}
464
                </p>
465
              </div>
466
            </div>
467
          </div>
468
        </AliasList>
469
      </div>
470
    </div>
471
  );
472
};
473

474
const StepThree = () => {
1✔
475
  const l10n = useL10n();
×
476

NEW
477
  const extensionGraphic = supportsFirefoxExtension() ? (
×
478
    <div className={styles["addon-content-items"]}>
479
      <Image src={Extension} alt="" />
480
      <div className={styles["addon-content-text"]}>
481
        <p className={styles.headline}>
482
          {l10n.getString("profile-free-onboarding-addon-item-headline-2")}
483
        </p>
484
        <p className={styles.description}>
485
          {l10n.getString("profile-free-onboarding-addon-item-description-2")}
486
        </p>
487
        <Image className={styles["small-arrow"]} src={SmallArrow} alt="" />
488
      </div>
489
    </div>
490
  ) : null;
491

492
  return (
493
    <div
494
      className={`${styles.step} ${styles["step-copy-mask"]} ${styles["mask-use"]}`}
495
    >
496
      <div className={styles["copy-mask-header"]}>
497
        <h1>{l10n.getString("profile-free-onboarding-addon-headline")}</h1>
498
        <p>{l10n.getString("profile-free-onboarding-addon-subheadline")}</p>
499
      </div>
500
      <div className={styles["addon-content-wrapper"]}>
501
        <div className={styles["addon-content-items"]}>
502
          <div className={styles["addon-content-text"]}>
503
            <p className={styles.headline}>
504
              {l10n.getString("profile-free-onboarding-addon-item-headline-1")}
505
            </p>
506
            <p className={styles.description}>
507
              {l10n.getString(
508
                "profile-free-onboarding-addon-item-description-1",
509
              )}
510
            </p>
511
            <Image className={styles["large-arrow"]} src={LargeArrow} alt="" />
512
          </div>
513
          <Image src={WorkingMan} alt="" />
514
        </div>
515
        {extensionGraphic}
516
      </div>
517
    </div>
518
  );
519
};
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