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

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

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

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

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

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

80
  if (props.profile.onboarding_free_state === 0) {
4✔
81
    const skipMaskCreation = () => {
1✔
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 () => {
1✔
92
      let moveToNextStep = true;
1✔
93

94
      try {
1✔
95
        await props.generateNewMask({ mask_type: "random" });
1✔
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) {
1!
104
        props.onNextStep(1);
1✔
105
        gaEvent({
1✔
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 />;
1✔
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) {
4✔
135
    const skipMaskTesting = () => {
2✔
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 = () => {
2✔
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 = () => {
2✔
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
        data-testid="open-forwarding-modal"
193
        onClick={() => {
194
          setIsModalOpen(true);
1✔
195
        }}
196
      >
197
        {l10n.getString(
198
          "profile-free-onboarding-copy-mask-how-forwarding-works",
199
        )}
200
      </Button>
201
    );
202

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

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

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

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

239
    step = <StepThree />;
1✔
240

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

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

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

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

344
const StepOne = () => {
2✔
345
  const l10n = useL10n();
1✔
346

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

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

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

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

480
const StepThree = () => {
2✔
481
  const l10n = useL10n();
1✔
482

483
  const extensionGraphic = supportsFirefoxExtension() ? (
1!
484
    <div className={styles["addon-content-items"]}>
485
      <Image src={Extension} alt="" />
486
      <div className={styles["addon-content-text"]}>
487
        <p className={styles.headline}>
488
          {l10n.getString("profile-free-onboarding-addon-item-headline-2")}
489
        </p>
490
        <p className={styles.description}>
491
          {l10n.getString("profile-free-onboarding-addon-item-description-2")}
492
        </p>
493
        <Image className={styles["small-arrow"]} src={SmallArrow} alt="" />
494
      </div>
495
    </div>
496
  ) : null;
497

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